This commit is contained in:
Hailey
2024-01-11 02:01:20 -08:00
parent c6d2e54923
commit 648552eafb
15 changed files with 132 additions and 78 deletions
+3 -25
View File
@@ -1,26 +1,4 @@
import { NativeModulesProxy, EventEmitter, Subscription } from 'expo-modules-core';
import ExpoSelectableTextView from './src/ExpoSelectableTextView'
import {ExpoProTextViewProps} from './src/ExpoSelectableText.types'
// Import the native module. On web, it will be resolved to ExpoSelectableText.web.ts
// and on native platforms to ExpoSelectableText.ts
import ExpoSelectableTextModule from './src/ExpoSelectableTextModule';
import ExpoSelectableTextView from './src/ExpoSelectableTextView';
import { ChangeEventPayload, ExpoSelectableTextViewProps } from './src/ExpoSelectableText.types';
// Get the native constant value.
export const PI = ExpoSelectableTextModule.PI;
export function hello(): string {
return ExpoSelectableTextModule.hello();
}
export async function setValueAsync(value: string) {
return await ExpoSelectableTextModule.setValueAsync(value);
}
const emitter = new EventEmitter(ExpoSelectableTextModule ?? NativeModulesProxy.ExpoSelectableText);
export function addChangeListener(listener: (event: ChangeEventPayload) => void): Subscription {
return emitter.addListener<ChangeEventPayload>('onChange', listener);
}
export { ExpoSelectableTextView, ExpoSelectableTextViewProps, ChangeEventPayload };
export {ExpoSelectableTextView as SelectableText, type ExpoProTextViewProps}
@@ -1,26 +0,0 @@
import ExpoModulesCore
public class ExpoProTextModule: Module {
public func definition() -> ModuleDefinition {
Name("ExpoProText")
View(ExpoProTextView.self) {
Events("onTextPress", "onTextLongPress")
Prop("segments") { (view: ExpoProTextView, prop: String) in
// Convert the JSON to segments
if let data = prop.data(using: .utf8) {
print("yes")
if let segments = try? JSONDecoder().decode(TextSegments.self, from: data) {
print("yes2")
view.segments = segments.segments
}
}
}
Prop("selectable") { (view: ExpoProTextView, prop: Bool) in
view.textView.isSelectable = prop
}
}
}
}
@@ -1,5 +1,5 @@
Pod::Spec.new do |s|
s.name = 'ExpoProText'
s.name = 'ExpoSelectableText'
s.version = '1.0.0'
s.summary = 'A sample project summary'
s.description = 'A sample project description'
@@ -16,6 +16,6 @@ Pod::Spec.new do |s|
'DEFINES_MODULE' => 'YES',
'SWIFT_COMPILATION_MODE' => 'wholemodule'
}
s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}"
end
@@ -0,0 +1,32 @@
import ExpoModulesCore
public class ExpoSelectableTextModule: Module {
public func definition() -> ModuleDefinition {
Name("ExpoSelectableText")
View(ExpoSelectableTextView.self) {
Events("onTextPress", "onTextLongPress", "onTextLayout")
Prop("segments") { (view: ExpoSelectableTextView, prop: String) in
// Convert the JSON to segments
if let data = prop.data(using: .utf8) {
if let segments = try? JSONDecoder().decode(TextSegments.self, from: data) {
view.segments = segments.segments
}
}
}
Prop("selectable") { (view: ExpoSelectableTextView, prop: Bool) in
view.textView.isSelectable = prop
}
Prop("rootStyle") { (view: ExpoSelectableTextView, prop: String) in
if let data = prop.data(using: .utf8) {
if let style = try? JSONDecoder().decode(TextStyle.self, from: data) {
view.style = style
}
}
}
}
}
}
@@ -1,4 +1,4 @@
public class ExpoProTextUtil {
public class ExpoSelectableTextUtil {
public static func hexToUIColor(hex: String?) -> UIColor {
guard let hex else {
return UIColor.black
@@ -1,13 +1,18 @@
import Foundation
import ExpoModulesCore
class ExpoProTextView: ExpoView {
class ExpoSelectableTextView: ExpoView {
let textView: UITextView = UITextView()
var segments: Array<TextSegment> = [] {
didSet {
setText()
}
}
var style: TextStyle? {
didSet {
self.setNeedsLayout()
}
}
let onTextPress = EventDispatcher()
let onTextLongPress = EventDispatcher()
@@ -22,6 +27,8 @@ class ExpoProTextView: ExpoView {
textView.isScrollEnabled = false
textView.backgroundColor = .clear
clipsToBounds = true
// Add the text view to the root view
addSubview(textView)
@@ -71,8 +78,8 @@ class ExpoProTextView: ExpoView {
segments.forEach { segment in
// Set some generic attributes that don't need ranges
let attributes: [NSAttributedString.Key:Any] = [
.font: UIFont.systemFont(ofSize: segment.style?.fontSize ?? 12.0, weight: segment.style?.fontWeight?.toFontWeight() ?? .regular),
.foregroundColor: ExpoProTextUtil.hexToUIColor(hex: segment.style?.color),
.font: UIFont.systemFont(ofSize: segment.style?.fontSize ?? self.style?.fontSize ?? 12.0, weight: segment.style?.fontWeight?.toFontWeight() ?? self.style?.fontWeight?.toFontWeight() ?? .regular),
.foregroundColor: ExpoSelectableTextUtil.hexToUIColor(hex: segment.style?.color),
]
// Create the attributed string with the generic attributes
@@ -81,7 +88,8 @@ class ExpoProTextView: ExpoView {
// Set the paragraph style attributes if necessary
if let lineHeight = segment.style?.lineHeight {
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = lineHeight
paragraphStyle.minimumLineHeight = lineHeight
paragraphStyle.maximumLineHeight = lineHeight
string.addAttribute(NSAttributedString.Key.paragraphStyle, value: paragraphStyle, range: NSMakeRange(0, string.length))
}
@@ -7,11 +7,21 @@ struct TextStyle: Decodable {
var textAlign: String? = "auto"
var lineHeight: Double?
var textDecorationLine: TextDecorationLine?
var flex: Int?
}
enum FontWeight: String, Decodable {
case bold
case normal
case one = "100"
case two = "200"
case three = "300"
case four = "400"
case five = "500"
case six = "600"
case seven = "700"
case eight = "800"
case nine = "900"
func toFontWeight() -> UIFont.Weight {
switch self {
@@ -19,6 +29,24 @@ enum FontWeight: String, Decodable {
return .bold
case .normal:
return .regular
case .one:
return .ultraLight
case .two:
return .ultraLight
case .three:
return .light
case .four:
return .regular
case .five:
return .medium
case .six:
return .semibold
case .seven:
return .semibold
case .eight:
return .bold
case .nine:
return .heavy
}
}
}
@@ -27,5 +55,6 @@ enum TextDecorationLine: String, Decodable {
case underline
case lineThrough = "line-through"
case underlineLineThrough = "underline line-through"
case normal
}
@@ -1,5 +1,5 @@
import React from 'react'
import { ColorValue } from 'react-native'
import {ColorValue, TextStyle, ViewStyle} from 'react-native'
interface ExpoProTextViewCommonProps {
selectable?: boolean
@@ -7,7 +7,7 @@ interface ExpoProTextViewCommonProps {
export interface ExpoProTextViewProps extends ExpoProTextViewCommonProps {
children: React.ReactNode
style?: ExpoProTextStyle
style?: TextStyle
onPress?: () => void
onLongPress?: () => void
}
@@ -19,6 +19,8 @@ export interface ExpoProTextNativeViewProps extends ExpoProTextViewCommonProps {
onTextLongPress?: (event: ExpoProTextPressEvent) => void
onTextLayout?: (event: ExpoProTextLayoutEvent) => void
disableLongPress: boolean
style: ViewStyle
rootStyle?: string
}
interface ExpoProTextStyle {
@@ -1,6 +1,6 @@
import * as React from 'react'
import { ExpoProTextViewProps } from './ExpoProText.types'
import {ExpoProTextViewProps} from './ExpoSelectableText.types'
export default function ExpoProTextView(props: ExpoProTextViewProps) {
return (
@@ -1,4 +1,4 @@
import { requireNativeViewManager } from 'expo-modules-core'
import {requireNativeViewManager} from 'expo-modules-core'
import * as React from 'react'
import {
@@ -7,37 +7,37 @@ import {
ExpoProTextPressEvent,
ExpoProTextSegment,
ExpoProTextViewProps,
} from './ExpoProText.types'
import { Text, View } from 'react-native'
} from './ExpoSelectableText.types'
import {StyleSheet, Text, View} from 'react-native'
const NativeView: React.ComponentType<ExpoProTextNativeViewProps> =
requireNativeViewManager('ExpoProText')
requireNativeViewManager('ExpoSelectableText')
export default function ExpoProTextView({
export default function ExpoSelectableTextView({
style,
children,
selectable = false,
onPress,
onLongPress,
}: ExpoProTextViewProps) {
const [dims, setDims] = React.useState({ height: 0 })
const [dims, setDims] = React.useState({height: 0})
const segmentPressCallbacks = React.useRef<
Array<{ index: number; onPress: () => void }>
Array<{index: number; onPress: () => void}>
>([])
const segmentLongPressCallbacks = React.useRef<
Array<{ index: number, onLongPress: () => void, }>
Array<{index: number; onLongPress: () => void}>
>([])
const onTextLayout = React.useCallback((e: ExpoProTextLayoutEvent) => {
console.log('layout')
setDims({
height: e.nativeEvent.height,
})
}, [])
const onTextPress = React.useCallback((e: ExpoProTextPressEvent) => {
console.log(e.nativeEvent)
const onPressSegment = segmentPressCallbacks.current.find(
(s) => s.index === e.nativeEvent.index,
s => s.index === e.nativeEvent.index,
)
onPressSegment?.onPress()
}, [])
@@ -58,7 +58,7 @@ export default function ExpoProTextView({
React.isValidElement(child) &&
(child as React.ReactElement<any>).type === Text
) {
const { onPress, onLongPress, children, style } = child.props
const {onPress, onLongPress, children, style} = child.props
segments.push({
index,
@@ -88,21 +88,25 @@ export default function ExpoProTextView({
}, [children])
const segmentsJson = React.useMemo(() => {
const json = JSON.stringify({ segments: textSegments })
console.log(json)
const json = JSON.stringify({segments: textSegments})
return json
}, [textSegments])
const rootStyle = React.useMemo(() => {
return style ? JSON.stringify(style) : undefined
}, [style])
return (
<View style={dims}>
<View style={[dims, {width: '100%'}]}>
<NativeView
textStyle={style}
segments={segmentsJson}
selectable={selectable}
onTextPress={onTextPress}
onTextLongPress={onLongPress}
onTextLayout={onTextLayout}
disableLongPress={onLongPress !== undefined}
style={{flex: 1}}
rootStyle={rootStyle}
/>
</View>
)
@@ -369,6 +369,7 @@ let PostThreadItemLoaded = ({
richText={richText}
lineHeight={1.3}
style={s.flex1}
selectable
/>
</View>
) : undefined}
+4 -1
View File
@@ -17,6 +17,7 @@ export function RichText({
lineHeight = 1.2,
style,
numberOfLines,
selectable,
}: {
testID?: string
type?: TypographyVariant
@@ -24,6 +25,7 @@ export function RichText({
lineHeight?: number
style?: StyleProp<TextStyle>
numberOfLines?: number
selectable?: boolean
}) {
const theme = useTheme()
const pal = usePalette('default')
@@ -54,7 +56,8 @@ export function RichText({
style={[style, pal.text, lineHeightStyle]}
numberOfLines={numberOfLines}
// @ts-ignore web only -prf
dataSet={WORD_WRAP}>
dataSet={WORD_WRAP}
selectable={selectable}>
{text}
</Text>
)
+24 -1
View File
@@ -1,13 +1,17 @@
import React from 'react'
import {Text as RNText, TextProps} from 'react-native'
import {StyleSheet, Text as RNText, TextProps} from 'react-native'
import {s, lh} from 'lib/styles'
import {useTheme, TypographyVariant} from 'lib/ThemeContext'
import {SelectableText} from '../../../../../modules/expo-selectable-text'
import {isIOS} from 'platform/detection'
import {fontSize} from '#/alf/tokens'
export type CustomTextProps = TextProps & {
type?: TypographyVariant
lineHeight?: number
title?: string
dataSet?: Record<string, string | number>
selectable?: boolean
}
export function Text({
@@ -17,11 +21,30 @@ export function Text({
style,
title,
dataSet,
selectable,
...props
}: React.PropsWithChildren<CustomTextProps>) {
const theme = useTheme()
const typography = theme.typography[type]
const lineHeightStyle = lineHeight ? lh(theme, type, lineHeight) : undefined
// {"color":"#000000","fontSize":20,"letterSpacing":0.2,"fontWeight":"400","flex":1,"lineHeight":26}
if (selectable && isIOS) {
return (
<SelectableText
selectable
style={StyleSheet.flatten([
s.black,
typography,
lineHeightStyle,
style,
])}>
{children}
</SelectableText>
)
}
return (
<RNText
style={[s.black, typography, lineHeightStyle, style]}