Upgrade `UITextView` to latest (#3090)
* uitextview use library w/ fixes bump bump multiple uitextview fixes * bump * update to latest * cleanupzio/stable
parent
a356b1be1a
commit
7fb117d213
|
@ -1,61 +0,0 @@
|
||||||
# React Native UITextView
|
|
||||||
|
|
||||||
Drop in replacement for `<Text>` that renders a `UITextView`, support selection and native translation features on iOS.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
In this project, no installation is required. The pod will be installed automatically during a `pod install`.
|
|
||||||
|
|
||||||
In another project, clone the repo and copy the `modules/react-native-ui-text-view` directory to your own project
|
|
||||||
directory. Afterward, run `pod install`.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
Replace the outermost `<Text>` with `<UITextView>`. Styles and press events should be handled the same way they would
|
|
||||||
with `<Text>`. Both `<UITextView>` and `<Text>` are supported as children of the root `<UITextView>`.
|
|
||||||
|
|
||||||
## Technical
|
|
||||||
|
|
||||||
React Native's `Text` component allows for "infinite" nesting of further `Text` components. To make a true "drop-in",
|
|
||||||
we want to do the same thing.
|
|
||||||
|
|
||||||
To achieve this, we first need to handle determining if we are dealing with an ancestor or root `UITextView` component.
|
|
||||||
We can implement similar logic to the `Text` component [see Text.js](https://github.com/facebook/react-native/blob/7f2529de7bc9ab1617eaf571e950d0717c3102a6/packages/react-native/Libraries/Text/Text.js).
|
|
||||||
|
|
||||||
We create a context that contains a boolean to tell us if we have already rendered the root `UITextView`. We also store
|
|
||||||
the root styles so that we can apply those styles if the ancestor `UITextView`s have not overwritten those styles.
|
|
||||||
|
|
||||||
All of our children are placed into `RNUITextView`, which is the main native view that will display the iOS `UITextView`.
|
|
||||||
|
|
||||||
We next map each child into the view. We have to be careful here to check if the child's `children` prop is a string. If
|
|
||||||
it is, that means we have encountered what was once an RN `Text` component. RN doesn't let us pass plain text as
|
|
||||||
children outside of `Text`, so we instead just pass the text into the `text` prop on `RNUITextViewChild`. We continue
|
|
||||||
down the tree, until we run out of children.
|
|
||||||
|
|
||||||
On the native side, we make use of the shadow view to calculate text container dimensions before the views are mounted.
|
|
||||||
We cannot simply set the `UITextView` text first, since React will not have properly measured the layout before this
|
|
||||||
occurs.
|
|
||||||
|
|
||||||
|
|
||||||
As for `Text` props, the following props are implemented:
|
|
||||||
|
|
||||||
- All accessibility props
|
|
||||||
- `allowFontScaling`
|
|
||||||
- `adjustsFontSizeToFit`
|
|
||||||
- `ellipsizeMode`
|
|
||||||
- `numberOfLines`
|
|
||||||
- `onLayout`
|
|
||||||
- `onPress`
|
|
||||||
- `onTextLayout`
|
|
||||||
- `selectable`
|
|
||||||
|
|
||||||
All `ViewStyle` props will apply to the root `UITextView`. Individual children will respect these `TextStyle` styles:
|
|
||||||
|
|
||||||
- `color`
|
|
||||||
- `fontSize`
|
|
||||||
- `fontStyle`
|
|
||||||
- `fontWeight`
|
|
||||||
- `fontVariant`
|
|
||||||
- `letterSpacing`
|
|
||||||
- `lineHeight`
|
|
||||||
- `textDecorationLine`
|
|
|
@ -1,3 +0,0 @@
|
||||||
#import <React/RCTViewManager.h>
|
|
||||||
#import <React/RCTBridge.h>
|
|
||||||
#import <React/RCTBridge+Private.h>
|
|
|
@ -1,153 +0,0 @@
|
||||||
class RNUITextView: UIView {
|
|
||||||
var textView: UITextView
|
|
||||||
|
|
||||||
@objc var numberOfLines: Int = 0 {
|
|
||||||
didSet {
|
|
||||||
textView.textContainer.maximumNumberOfLines = numberOfLines
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@objc var selectable: Bool = true {
|
|
||||||
didSet {
|
|
||||||
textView.isSelectable = selectable
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@objc var ellipsizeMode: String = "tail" {
|
|
||||||
didSet {
|
|
||||||
textView.textContainer.lineBreakMode = self.getLineBreakMode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@objc var onTextLayout: RCTDirectEventBlock?
|
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
|
||||||
if #available(iOS 16.0, *) {
|
|
||||||
textView = UITextView(usingTextLayoutManager: false)
|
|
||||||
} else {
|
|
||||||
textView = UITextView()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disable scrolling
|
|
||||||
textView.isScrollEnabled = false
|
|
||||||
// Remove all the padding
|
|
||||||
textView.textContainerInset = .zero
|
|
||||||
textView.textContainer.lineFragmentPadding = 0
|
|
||||||
|
|
||||||
// Remove other properties
|
|
||||||
textView.isEditable = false
|
|
||||||
textView.backgroundColor = .clear
|
|
||||||
|
|
||||||
// Init
|
|
||||||
super.init(frame: frame)
|
|
||||||
self.clipsToBounds = true
|
|
||||||
|
|
||||||
// Add the view
|
|
||||||
addSubview(textView)
|
|
||||||
|
|
||||||
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(callOnPress(_:)))
|
|
||||||
tapGestureRecognizer.isEnabled = true
|
|
||||||
textView.addGestureRecognizer(tapGestureRecognizer)
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolves some animation issues
|
|
||||||
override func reactSetFrame(_ frame: CGRect) {
|
|
||||||
UIView.performWithoutAnimation {
|
|
||||||
super.reactSetFrame(frame)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func setText(string: NSAttributedString, size: CGSize, numberOfLines: Int) -> Void {
|
|
||||||
self.textView.frame.size = size
|
|
||||||
self.textView.textContainer.maximumNumberOfLines = numberOfLines
|
|
||||||
self.textView.attributedText = string
|
|
||||||
self.textView.selectedTextRange = nil
|
|
||||||
|
|
||||||
if let onTextLayout = self.onTextLayout {
|
|
||||||
var lines: [String] = []
|
|
||||||
textView.layoutManager.enumerateLineFragments(
|
|
||||||
forGlyphRange: NSRange(location: 0, length: textView.attributedText.length))
|
|
||||||
{ (rect, usedRect, textContainer, glyphRange, stop) in
|
|
||||||
let characterRange = self.textView.layoutManager.characterRange(forGlyphRange: glyphRange, actualGlyphRange: nil)
|
|
||||||
let line = (self.textView.text as NSString).substring(with: characterRange)
|
|
||||||
lines.append(line)
|
|
||||||
}
|
|
||||||
|
|
||||||
onTextLayout([
|
|
||||||
"lines": lines
|
|
||||||
])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@IBAction func callOnPress(_ sender: UITapGestureRecognizer) -> Void {
|
|
||||||
// If we find a child, then call onPress
|
|
||||||
if let child = getPressed(sender) {
|
|
||||||
if textView.selectedTextRange == nil, let onPress = child.onPress {
|
|
||||||
onPress(["": ""])
|
|
||||||
} else {
|
|
||||||
// Clear the selected text range if we are not pressing on a link
|
|
||||||
textView.selectedTextRange = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to get the pressed segment
|
|
||||||
func getPressed(_ sender: UITapGestureRecognizer) -> RNUITextViewChild? {
|
|
||||||
let layoutManager = textView.layoutManager
|
|
||||||
var location = sender.location(in: textView)
|
|
||||||
|
|
||||||
// Remove the padding
|
|
||||||
location.x -= textView.textContainerInset.left
|
|
||||||
location.y -= textView.textContainerInset.top
|
|
||||||
|
|
||||||
// Get the index of the char
|
|
||||||
let charIndex = layoutManager.characterIndex(
|
|
||||||
for: location,
|
|
||||||
in: textView.textContainer,
|
|
||||||
fractionOfDistanceBetweenInsertionPoints: nil
|
|
||||||
)
|
|
||||||
|
|
||||||
var lastUpperBound: String.Index? = nil
|
|
||||||
for child in self.reactSubviews() {
|
|
||||||
if let child = child as? RNUITextViewChild, let childText = child.text {
|
|
||||||
let fullText = self.textView.attributedText.string
|
|
||||||
|
|
||||||
// We want to skip over the children we have already checked, otherwise we could run into
|
|
||||||
// collisions of similar strings (i.e. links that get shortened to the same hostname but
|
|
||||||
// different paths)
|
|
||||||
let range = fullText.range(of: childText, options: [], range: (lastUpperBound ?? String.Index(utf16Offset: 0, in: fullText) )..<fullText.endIndex)
|
|
||||||
|
|
||||||
if let lowerBound = range?.lowerBound, let upperBound = range?.upperBound {
|
|
||||||
let lowerOffset = lowerBound.utf16Offset(in: fullText)
|
|
||||||
let upperOffset = upperBound.utf16Offset(in: fullText)
|
|
||||||
|
|
||||||
if charIndex >= lowerOffset,
|
|
||||||
charIndex <= upperOffset
|
|
||||||
{
|
|
||||||
return child
|
|
||||||
} else {
|
|
||||||
lastUpperBound = upperBound
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getLineBreakMode() -> NSLineBreakMode {
|
|
||||||
switch self.ellipsizeMode {
|
|
||||||
case "head":
|
|
||||||
return .byTruncatingHead
|
|
||||||
case "middle":
|
|
||||||
return .byTruncatingMiddle
|
|
||||||
case "tail":
|
|
||||||
return .byTruncatingTail
|
|
||||||
case "clip":
|
|
||||||
return .byClipping
|
|
||||||
default:
|
|
||||||
return .byTruncatingTail
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
class RNUITextViewChild: UIView {
|
|
||||||
@objc var text: String?
|
|
||||||
@objc var onPress: RCTDirectEventBlock?
|
|
||||||
}
|
|
|
@ -1,56 +0,0 @@
|
||||||
// We want all of our props to be available in the child's shadow view so we
|
|
||||||
// can create the attributed text before mount and calculate the needed size
|
|
||||||
// for the view.
|
|
||||||
class RNUITextViewChildShadow: RCTShadowView {
|
|
||||||
@objc var text: String = ""
|
|
||||||
@objc var color: UIColor = .black
|
|
||||||
@objc var fontSize: CGFloat = 16.0
|
|
||||||
@objc var fontStyle: String = "normal"
|
|
||||||
@objc var fontWeight: String = "normal"
|
|
||||||
@objc var letterSpacing: CGFloat = 0.0
|
|
||||||
@objc var lineHeight: CGFloat = 0.0
|
|
||||||
@objc var pointerEvents: NSString?
|
|
||||||
|
|
||||||
override func isYogaLeafNode() -> Bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override func didSetProps(_ changedProps: [String]!) {
|
|
||||||
guard let superview = self.superview as? RNUITextViewShadow else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !YGNodeIsDirty(superview.yogaNode) {
|
|
||||||
superview.setAttributedText()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getFontWeight() -> UIFont.Weight {
|
|
||||||
switch self.fontWeight {
|
|
||||||
case "bold":
|
|
||||||
return .bold
|
|
||||||
case "normal":
|
|
||||||
return .regular
|
|
||||||
case "100":
|
|
||||||
return .ultraLight
|
|
||||||
case "200":
|
|
||||||
return .ultraLight
|
|
||||||
case "300":
|
|
||||||
return .light
|
|
||||||
case "400":
|
|
||||||
return .regular
|
|
||||||
case "500":
|
|
||||||
return .medium
|
|
||||||
case "600":
|
|
||||||
return .semibold
|
|
||||||
case "700":
|
|
||||||
return .semibold
|
|
||||||
case "800":
|
|
||||||
return .bold
|
|
||||||
case "900":
|
|
||||||
return .heavy
|
|
||||||
default:
|
|
||||||
return .regular
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
#import <React/RCTViewManager.h>
|
|
||||||
|
|
||||||
@interface RCT_EXTERN_MODULE(RNUITextViewManager, RCTViewManager)
|
|
||||||
RCT_REMAP_SHADOW_PROPERTY(numberOfLines, numberOfLines, NSInteger)
|
|
||||||
RCT_REMAP_SHADOW_PROPERTY(allowsFontScaling, allowsFontScaling, BOOL)
|
|
||||||
|
|
||||||
RCT_EXPORT_VIEW_PROPERTY(numberOfLines, NSInteger)
|
|
||||||
RCT_EXPORT_VIEW_PROPERTY(onTextLayout, RCTDirectEventBlock)
|
|
||||||
RCT_EXPORT_VIEW_PROPERTY(ellipsizeMode, NSString)
|
|
||||||
RCT_EXPORT_VIEW_PROPERTY(selectable, BOOL)
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
@interface RCT_EXTERN_MODULE(RNUITextViewChildManager, RCTViewManager)
|
|
||||||
RCT_REMAP_SHADOW_PROPERTY(text, text, NSString)
|
|
||||||
RCT_REMAP_SHADOW_PROPERTY(color, color, UIColor)
|
|
||||||
RCT_REMAP_SHADOW_PROPERTY(fontSize, fontSize, CGFloat)
|
|
||||||
RCT_REMAP_SHADOW_PROPERTY(fontStyle, fontStyle, NSString)
|
|
||||||
RCT_REMAP_SHADOW_PROPERTY(fontWeight, fontWeight, NSString)
|
|
||||||
RCT_REMAP_SHADOW_PROPERTY(letterSpacing, letterSpacing, CGFloat)
|
|
||||||
RCT_REMAP_SHADOW_PROPERTY(lineHeight, lineHeight, CGFloat)
|
|
||||||
RCT_REMAP_SHADOW_PROPERTY(pointerEvents, pointerEvents, NSString)
|
|
||||||
|
|
||||||
RCT_EXPORT_VIEW_PROPERTY(text, NSString)
|
|
||||||
RCT_EXPORT_VIEW_PROPERTY(onPress, RCTBubblingEventBlock)
|
|
||||||
@end
|
|
|
@ -1,30 +0,0 @@
|
||||||
@objc(RNUITextViewManager)
|
|
||||||
class RNUITextViewManager: RCTViewManager {
|
|
||||||
override func view() -> (RNUITextView) {
|
|
||||||
return RNUITextView()
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc override static func requiresMainQueueSetup() -> Bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override func shadowView() -> RCTShadowView {
|
|
||||||
// Pass the bridge to the shadow view
|
|
||||||
return RNUITextViewShadow(bridge: self.bridge)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc(RNUITextViewChildManager)
|
|
||||||
class RNUITextViewChildManager: RCTViewManager {
|
|
||||||
override func view() -> (RNUITextViewChild) {
|
|
||||||
return RNUITextViewChild()
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc override static func requiresMainQueueSetup() -> Bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override func shadowView() -> RCTShadowView {
|
|
||||||
return RNUITextViewChildShadow()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,152 +0,0 @@
|
||||||
class RNUITextViewShadow: RCTShadowView {
|
|
||||||
// Props
|
|
||||||
@objc var numberOfLines: Int = 0 {
|
|
||||||
didSet {
|
|
||||||
if !YGNodeIsDirty(self.yogaNode) {
|
|
||||||
self.setAttributedText()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@objc var allowsFontScaling: Bool = true
|
|
||||||
|
|
||||||
var attributedText: NSAttributedString = NSAttributedString()
|
|
||||||
var frameSize: CGSize = CGSize()
|
|
||||||
|
|
||||||
var lineHeight: CGFloat = 0
|
|
||||||
|
|
||||||
var bridge: RCTBridge
|
|
||||||
|
|
||||||
init(bridge: RCTBridge) {
|
|
||||||
self.bridge = bridge
|
|
||||||
super.init()
|
|
||||||
|
|
||||||
// We need to set a custom measure func here to calculate the height correctly
|
|
||||||
YGNodeSetMeasureFunc(self.yogaNode) { node, width, widthMode, height, heightMode in
|
|
||||||
// Get the shadowview and determine the needed size to set
|
|
||||||
let shadowView = Unmanaged<RNUITextViewShadow>.fromOpaque(YGNodeGetContext(node)).takeUnretainedValue()
|
|
||||||
return shadowView.getNeededSize(maxWidth: width)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subscribe to ynamic type size changes
|
|
||||||
NotificationCenter.default.addObserver(
|
|
||||||
self,
|
|
||||||
selector: #selector(preferredContentSizeChanged(_:)),
|
|
||||||
name: UIContentSizeCategory.didChangeNotification,
|
|
||||||
object: nil
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc func preferredContentSizeChanged(_ notification: Notification) {
|
|
||||||
self.setAttributedText()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returning true here will tell Yoga to not use flexbox and instead use our custom measure func.
|
|
||||||
override func isYogaLeafNode() -> Bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// We should only insert children that are UITextView shadows
|
|
||||||
override func insertReactSubview(_ subview: RCTShadowView!, at atIndex: Int) {
|
|
||||||
if subview.isKind(of: RNUITextViewChildShadow.self) {
|
|
||||||
super.insertReactSubview(subview, at: atIndex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Every time the subviews change, we need to reformat and render the text.
|
|
||||||
override func didUpdateReactSubviews() {
|
|
||||||
self.setAttributedText()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Whenever we layout, update the UI
|
|
||||||
override func layoutSubviews(with layoutContext: RCTLayoutContext) {
|
|
||||||
// Don't do anything if the layout is dirty
|
|
||||||
if(YGNodeIsDirty(self.yogaNode)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Since we are inside the shadow view here, we have to find the real view and update the text.
|
|
||||||
self.bridge.uiManager.addUIBlock { uiManager, viewRegistry in
|
|
||||||
guard let textView = viewRegistry?[self.reactTag] as? RNUITextView else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
textView.setText(string: self.attributedText, size: self.frameSize, numberOfLines: self.numberOfLines)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override func dirtyLayout() {
|
|
||||||
super.dirtyLayout()
|
|
||||||
YGNodeMarkDirty(self.yogaNode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the attributed text whenever changes are made to the subviews.
|
|
||||||
func setAttributedText() -> Void {
|
|
||||||
// Create an attributed string to store each of the segments
|
|
||||||
let finalAttributedString = NSMutableAttributedString()
|
|
||||||
|
|
||||||
self.reactSubviews().forEach { child in
|
|
||||||
guard let child = child as? RNUITextViewChildShadow else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let scaledFontSize = self.allowsFontScaling ?
|
|
||||||
UIFontMetrics.default.scaledValue(for: child.fontSize) : child.fontSize
|
|
||||||
let font = UIFont.systemFont(ofSize: scaledFontSize, weight: child.getFontWeight())
|
|
||||||
|
|
||||||
// Set some generic attributes that don't need ranges
|
|
||||||
let attributes: [NSAttributedString.Key:Any] = [
|
|
||||||
.font: font,
|
|
||||||
.foregroundColor: child.color,
|
|
||||||
]
|
|
||||||
|
|
||||||
// Create the attributed string with the generic attributes
|
|
||||||
let string = NSMutableAttributedString(string: child.text, attributes: attributes)
|
|
||||||
|
|
||||||
// Set the paragraph style attributes if necessary. We can check this by seeing if the provided
|
|
||||||
// line height is not 0.0.
|
|
||||||
let paragraphStyle = NSMutableParagraphStyle()
|
|
||||||
if child.lineHeight != 0.0 {
|
|
||||||
// Whenever we change the line height for the text, we are also removing the DynamicType
|
|
||||||
// adjustment for line height. We need to get the multiplier and apply that to the
|
|
||||||
// line height.
|
|
||||||
let scaleMultiplier = scaledFontSize / child.fontSize
|
|
||||||
paragraphStyle.minimumLineHeight = child.lineHeight * scaleMultiplier
|
|
||||||
paragraphStyle.maximumLineHeight = child.lineHeight * scaleMultiplier
|
|
||||||
|
|
||||||
string.addAttribute(
|
|
||||||
NSAttributedString.Key.paragraphStyle,
|
|
||||||
value: paragraphStyle,
|
|
||||||
range: NSMakeRange(0, string.length)
|
|
||||||
)
|
|
||||||
|
|
||||||
// To calcualte the size of the text without creating a new UILabel or UITextView, we have
|
|
||||||
// to store this line height for later.
|
|
||||||
self.lineHeight = child.lineHeight
|
|
||||||
} else {
|
|
||||||
self.lineHeight = font.lineHeight
|
|
||||||
}
|
|
||||||
|
|
||||||
finalAttributedString.append(string)
|
|
||||||
}
|
|
||||||
|
|
||||||
self.attributedText = finalAttributedString
|
|
||||||
self.dirtyLayout()
|
|
||||||
}
|
|
||||||
|
|
||||||
// To create the needed size we need to:
|
|
||||||
// 1. Get the max size that we can use for the view
|
|
||||||
// 2. Calculate the height of the text based on that max size
|
|
||||||
// 3. Determine how many lines the text is, and limit that number if it exceeds the max
|
|
||||||
// 4. Set the frame size and return the YGSize. YGSize requires Float values while CGSize needs CGFloat
|
|
||||||
func getNeededSize(maxWidth: Float) -> YGSize {
|
|
||||||
let maxSize = CGSize(width: CGFloat(maxWidth), height: CGFloat(MAXFLOAT))
|
|
||||||
let textSize = self.attributedText.boundingRect(with: maxSize, options: .usesLineFragmentOrigin, context: nil)
|
|
||||||
|
|
||||||
var totalLines = Int(ceil(textSize.height / self.lineHeight))
|
|
||||||
|
|
||||||
if self.numberOfLines != 0, totalLines > self.numberOfLines {
|
|
||||||
totalLines = self.numberOfLines
|
|
||||||
}
|
|
||||||
|
|
||||||
self.frameSize = CGSize(width: CGFloat(maxWidth), height: CGFloat(CGFloat(totalLines) * self.lineHeight))
|
|
||||||
return YGSize(width: Float(self.frameSize.width), height: Float(self.frameSize.height))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
{
|
|
||||||
"name": "react-native-ui-text-view",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"description": "UITextView in React Native on iOS",
|
|
||||||
"main": "src/index",
|
|
||||||
"author": "haileyok",
|
|
||||||
"license": "MIT",
|
|
||||||
"homepage": "https://github.com/bluesky-social/social-app/modules/react-native-ui-text-view"
|
|
||||||
}
|
|
|
@ -1,42 +0,0 @@
|
||||||
require "json"
|
|
||||||
|
|
||||||
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
|
||||||
folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32'
|
|
||||||
|
|
||||||
Pod::Spec.new do |s|
|
|
||||||
s.name = "react-native-ui-text-view"
|
|
||||||
s.version = package["version"]
|
|
||||||
s.summary = package["description"]
|
|
||||||
s.homepage = package["homepage"]
|
|
||||||
s.license = package["license"]
|
|
||||||
s.authors = package["author"]
|
|
||||||
|
|
||||||
s.platforms = { :ios => "11.0" }
|
|
||||||
s.source = { :git => ".git", :tag => "#{s.version}" }
|
|
||||||
|
|
||||||
s.source_files = "ios/**/*.{h,m,mm,swift}"
|
|
||||||
|
|
||||||
# Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0.
|
|
||||||
# See https://github.com/facebook/react-native/blob/febf6b7f33fdb4904669f99d795eba4c0f95d7bf/scripts/cocoapods/new_architecture.rb#L79.
|
|
||||||
if respond_to?(:install_modules_dependencies, true)
|
|
||||||
install_modules_dependencies(s)
|
|
||||||
else
|
|
||||||
s.dependency "React-Core"
|
|
||||||
|
|
||||||
# Don't install the dependencies when we run `pod install` in the old architecture.
|
|
||||||
if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then
|
|
||||||
s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1"
|
|
||||||
s.pod_target_xcconfig = {
|
|
||||||
"HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\"",
|
|
||||||
"OTHER_CPLUSPLUSFLAGS" => "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1",
|
|
||||||
"CLANG_CXX_LANGUAGE_STANDARD" => "c++17"
|
|
||||||
}
|
|
||||||
s.dependency "React-RCTFabric"
|
|
||||||
s.dependency "React-Codegen"
|
|
||||||
s.dependency "RCT-Folly"
|
|
||||||
s.dependency "RCTRequired"
|
|
||||||
s.dependency "RCTTypeSafety"
|
|
||||||
s.dependency "ReactCommon/turbomodule/core"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,76 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
import {Platform, StyleSheet, TextProps, ViewStyle} from 'react-native'
|
|
||||||
import {RNUITextView, RNUITextViewChild} from './index'
|
|
||||||
|
|
||||||
const TextAncestorContext = React.createContext<[boolean, ViewStyle]>([
|
|
||||||
false,
|
|
||||||
StyleSheet.create({}),
|
|
||||||
])
|
|
||||||
const useTextAncestorContext = () => React.useContext(TextAncestorContext)
|
|
||||||
|
|
||||||
const textDefaults: TextProps = {
|
|
||||||
allowFontScaling: true,
|
|
||||||
selectable: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
export function UITextView({style, children, ...rest}: TextProps) {
|
|
||||||
const [isAncestor, rootStyle] = useTextAncestorContext()
|
|
||||||
|
|
||||||
// Flatten the styles, and apply the root styles when needed
|
|
||||||
const flattenedStyle = React.useMemo(
|
|
||||||
() => StyleSheet.flatten([rootStyle, style]),
|
|
||||||
[rootStyle, style],
|
|
||||||
)
|
|
||||||
|
|
||||||
if (Platform.OS !== 'ios') {
|
|
||||||
throw new Error('UITextView is only available on iOS')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isAncestor) {
|
|
||||||
return (
|
|
||||||
<TextAncestorContext.Provider value={[true, flattenedStyle]}>
|
|
||||||
<RNUITextView
|
|
||||||
{...textDefaults}
|
|
||||||
{...rest}
|
|
||||||
ellipsizeMode={rest.ellipsizeMode ?? rest.lineBreakMode ?? 'tail'}
|
|
||||||
style={[{flex: 1}, flattenedStyle]}
|
|
||||||
onPress={undefined} // We want these to go to the children only
|
|
||||||
onLongPress={undefined}>
|
|
||||||
{React.Children.toArray(children).map((c, index) => {
|
|
||||||
if (React.isValidElement(c)) {
|
|
||||||
return c
|
|
||||||
} else if (typeof c === 'string') {
|
|
||||||
return (
|
|
||||||
<RNUITextViewChild
|
|
||||||
key={index}
|
|
||||||
style={flattenedStyle}
|
|
||||||
text={c}
|
|
||||||
{...rest}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})}
|
|
||||||
</RNUITextView>
|
|
||||||
</TextAncestorContext.Provider>
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{React.Children.toArray(children).map((c, index) => {
|
|
||||||
if (React.isValidElement(c)) {
|
|
||||||
return c
|
|
||||||
} else if (typeof c === 'string') {
|
|
||||||
return (
|
|
||||||
<RNUITextViewChild
|
|
||||||
key={index}
|
|
||||||
style={flattenedStyle}
|
|
||||||
text={c}
|
|
||||||
{...rest}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,42 +0,0 @@
|
||||||
import {
|
|
||||||
requireNativeComponent,
|
|
||||||
UIManager,
|
|
||||||
Platform,
|
|
||||||
type ViewStyle,
|
|
||||||
TextProps,
|
|
||||||
} from 'react-native'
|
|
||||||
|
|
||||||
const LINKING_ERROR =
|
|
||||||
`The package 'react-native-ui-text-view' doesn't seem to be linked. Make sure: \n\n` +
|
|
||||||
Platform.select({ios: "- You have run 'pod install'\n", default: ''}) +
|
|
||||||
'- You rebuilt the app after installing the package\n' +
|
|
||||||
'- You are not using Expo Go\n'
|
|
||||||
|
|
||||||
export interface RNUITextViewProps extends TextProps {
|
|
||||||
children: React.ReactNode
|
|
||||||
style: ViewStyle[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RNUITextViewChildProps extends TextProps {
|
|
||||||
text: string
|
|
||||||
onTextPress?: (...args: any[]) => void
|
|
||||||
onTextLongPress?: (...args: any[]) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const RNUITextView =
|
|
||||||
UIManager.getViewManagerConfig &&
|
|
||||||
UIManager.getViewManagerConfig('RNUITextView') != null
|
|
||||||
? requireNativeComponent<RNUITextViewProps>('RNUITextView')
|
|
||||||
: () => {
|
|
||||||
throw new Error(LINKING_ERROR)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const RNUITextViewChild =
|
|
||||||
UIManager.getViewManagerConfig &&
|
|
||||||
UIManager.getViewManagerConfig('RNUITextViewChild') != null
|
|
||||||
? requireNativeComponent<RNUITextViewChildProps>('RNUITextViewChild')
|
|
||||||
: () => {
|
|
||||||
throw new Error(LINKING_ERROR)
|
|
||||||
}
|
|
||||||
|
|
||||||
export * from './UITextView'
|
|
|
@ -174,7 +174,7 @@
|
||||||
"react-native-safe-area-context": "4.8.2",
|
"react-native-safe-area-context": "4.8.2",
|
||||||
"react-native-screens": "~3.29.0",
|
"react-native-screens": "~3.29.0",
|
||||||
"react-native-svg": "14.1.0",
|
"react-native-svg": "14.1.0",
|
||||||
"react-native-ui-text-view": "link:./modules/react-native-ui-text-view",
|
"react-native-uitextview": "^1.1.6",
|
||||||
"react-native-url-polyfill": "^1.3.0",
|
"react-native-url-polyfill": "^1.3.0",
|
||||||
"react-native-uuid": "^2.0.1",
|
"react-native-uuid": "^2.0.1",
|
||||||
"react-native-version-number": "^0.3.6",
|
"react-native-version-number": "^0.3.6",
|
||||||
|
|
|
@ -1,14 +1,9 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {
|
import {StyleProp, TextProps as RNTextProps, TextStyle} from 'react-native'
|
||||||
Text as RNText,
|
import {UITextView} from 'react-native-uitextview'
|
||||||
StyleProp,
|
|
||||||
TextStyle,
|
|
||||||
TextProps as RNTextProps,
|
|
||||||
} from 'react-native'
|
|
||||||
import {UITextView} from 'react-native-ui-text-view'
|
|
||||||
|
|
||||||
import {useTheme, atoms, web, flatten} from '#/alf'
|
import {isNative} from '#/platform/detection'
|
||||||
import {isIOS, isNative} from '#/platform/detection'
|
import {atoms, flatten, useTheme, web} from '#/alf'
|
||||||
|
|
||||||
export type TextProps = RNTextProps & {
|
export type TextProps = RNTextProps & {
|
||||||
/**
|
/**
|
||||||
|
@ -61,11 +56,8 @@ export function normalizeTextStyles(styles: StyleProp<TextStyle>) {
|
||||||
export function Text({style, selectable, ...rest}: TextProps) {
|
export function Text({style, selectable, ...rest}: TextProps) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const s = normalizeTextStyles([atoms.text_sm, t.atoms.text, flatten(style)])
|
const s = normalizeTextStyles([atoms.text_sm, t.atoms.text, flatten(style)])
|
||||||
return selectable && isIOS ? (
|
|
||||||
<UITextView style={s} {...rest} />
|
return <UITextView selectable={selectable} uiTextView style={s} {...rest} />
|
||||||
) : (
|
|
||||||
<RNText selectable={selectable} style={s} {...rest} />
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createHeadingElement({level}: {level: number}) {
|
export function createHeadingElement({level}: {level: number}) {
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {Text as RNText, TextProps} from 'react-native'
|
import {Text as RNText, TextProps} from 'react-native'
|
||||||
import {s, lh} from 'lib/styles'
|
import {UITextView} from 'react-native-uitextview'
|
||||||
import {useTheme, TypographyVariant} from 'lib/ThemeContext'
|
|
||||||
|
import {lh, s} from 'lib/styles'
|
||||||
|
import {TypographyVariant, useTheme} from 'lib/ThemeContext'
|
||||||
import {isIOS, isWeb} from 'platform/detection'
|
import {isIOS, isWeb} from 'platform/detection'
|
||||||
import {UITextView} from 'react-native-ui-text-view'
|
|
||||||
|
|
||||||
export type CustomTextProps = TextProps & {
|
export type CustomTextProps = TextProps & {
|
||||||
type?: TypographyVariant
|
type?: TypographyVariant
|
||||||
|
@ -36,6 +37,8 @@ export function Text({
|
||||||
return (
|
return (
|
||||||
<UITextView
|
<UITextView
|
||||||
style={[s.black, typography, lineHeightStyle, style]}
|
style={[s.black, typography, lineHeightStyle, style]}
|
||||||
|
selectable={selectable}
|
||||||
|
uiTextView
|
||||||
{...props}>
|
{...props}>
|
||||||
{children}
|
{children}
|
||||||
</UITextView>
|
</UITextView>
|
||||||
|
|
|
@ -18721,9 +18721,10 @@ react-native-svg@14.1.0:
|
||||||
css-select "^5.1.0"
|
css-select "^5.1.0"
|
||||||
css-tree "^1.1.3"
|
css-tree "^1.1.3"
|
||||||
|
|
||||||
"react-native-ui-text-view@link:./modules/react-native-ui-text-view":
|
react-native-uitextview@^1.1.6:
|
||||||
version "0.0.0"
|
version "1.1.6"
|
||||||
uid ""
|
resolved "https://registry.yarnpkg.com/react-native-uitextview/-/react-native-uitextview-1.1.6.tgz#a70d039f415158445c90de8e8e546a7c3b251d6d"
|
||||||
|
integrity sha512-OTGTw4Y2DDn4dHTwN7aKOndXP6NoS/AS35Rj/Rsss+KRsGHToiv2g3ZdzQ0ZhZabhwl1u+Oht+wSU/FU+SoJ+Q==
|
||||||
|
|
||||||
react-native-url-polyfill@^1.3.0:
|
react-native-url-polyfill@^1.3.0:
|
||||||
version "1.3.0"
|
version "1.3.0"
|
||||||
|
|
Loading…
Reference in New Issue