diff --git a/.gitignore b/.gitignore
index 7a6137b190..f96d0d5ff7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -91,8 +91,8 @@ web-build/
# Android & iOS folders
-android/
-ios/
+/android/
+/ios/
# environment variables
.env
diff --git a/modules/expo-selectable-text/android/build.gradle b/modules/expo-selectable-text/android/build.gradle
new file mode 100644
index 0000000000..9e85c5c2d8
--- /dev/null
+++ b/modules/expo-selectable-text/android/build.gradle
@@ -0,0 +1,89 @@
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+apply plugin: 'maven-publish'
+
+group = 'expo.modules.selectabletext'
+version = '0.2.0'
+
+buildscript {
+ def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
+ if (expoModulesCorePlugin.exists()) {
+ apply from: expoModulesCorePlugin
+ applyKotlinExpoModulesCorePlugin()
+ }
+
+ // Simple helper that allows the root project to override versions declared by this library.
+ ext.safeExtGet = { prop, fallback ->
+ rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
+ }
+
+ // Ensures backward compatibility
+ ext.getKotlinVersion = {
+ if (ext.has("kotlinVersion")) {
+ ext.kotlinVersion()
+ } else {
+ ext.safeExtGet("kotlinVersion", "1.8.10")
+ }
+ }
+
+ repositories {
+ mavenCentral()
+ }
+
+ dependencies {
+ classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${getKotlinVersion()}")
+ }
+}
+
+afterEvaluate {
+ publishing {
+ publications {
+ release(MavenPublication) {
+ from components.release
+ }
+ }
+ repositories {
+ maven {
+ url = mavenLocal().url
+ }
+ }
+ }
+}
+
+android {
+ compileSdkVersion safeExtGet("compileSdkVersion", 33)
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_11
+ targetCompatibility JavaVersion.VERSION_11
+ }
+
+ kotlinOptions {
+ jvmTarget = JavaVersion.VERSION_11.majorVersion
+ }
+
+ namespace "expo.modules.selectabletext"
+ defaultConfig {
+ minSdkVersion safeExtGet("minSdkVersion", 21)
+ targetSdkVersion safeExtGet("targetSdkVersion", 33)
+ versionCode 1
+ versionName "0.2.0"
+ }
+ lintOptions {
+ abortOnError false
+ }
+ publishing {
+ singleVariant("release") {
+ withSourcesJar()
+ }
+ }
+}
+
+repositories {
+ mavenCentral()
+}
+
+dependencies {
+ implementation project(':expo-modules-core')
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}"
+}
diff --git a/modules/expo-selectable-text/android/src/main/AndroidManifest.xml b/modules/expo-selectable-text/android/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..bdae66c8f5
--- /dev/null
+++ b/modules/expo-selectable-text/android/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/modules/expo-selectable-text/android/src/main/java/expo/modules/selectabletext/ExpoSelectableTextModule.kt b/modules/expo-selectable-text/android/src/main/java/expo/modules/selectabletext/ExpoSelectableTextModule.kt
new file mode 100644
index 0000000000..37f5f89e9a
--- /dev/null
+++ b/modules/expo-selectable-text/android/src/main/java/expo/modules/selectabletext/ExpoSelectableTextModule.kt
@@ -0,0 +1,47 @@
+package expo.modules.selectabletext
+
+import expo.modules.kotlin.modules.Module
+import expo.modules.kotlin.modules.ModuleDefinition
+
+class ExpoSelectableTextModule : Module() {
+ // Each module class must implement the definition function. The definition consists of components
+ // that describes the module's functionality and behavior.
+ // See https://docs.expo.dev/modules/module-api for more details about available components.
+ override fun definition() = ModuleDefinition {
+ // Sets the name of the module that JavaScript code will use to refer to the module. Takes a string as an argument.
+ // Can be inferred from module's class name, but it's recommended to set it explicitly for clarity.
+ // The module will be accessible from `requireNativeModule('ExpoSelectableText')` in JavaScript.
+ Name("ExpoSelectableText")
+
+ // Sets constant properties on the module. Can take a dictionary or a closure that returns a dictionary.
+ Constants(
+ "PI" to Math.PI
+ )
+
+ // Defines event names that the module can send to JavaScript.
+ Events("onChange")
+
+ // Defines a JavaScript synchronous function that runs the native code on the JavaScript thread.
+ Function("hello") {
+ "Hello world! ๐"
+ }
+
+ // Defines a JavaScript function that always returns a Promise and whose native code
+ // is by default dispatched on the different thread than the JavaScript runtime runs on.
+ AsyncFunction("setValueAsync") { value: String ->
+ // Send an event to JavaScript.
+ sendEvent("onChange", mapOf(
+ "value" to value
+ ))
+ }
+
+ // Enables the module to be used as a native view. Definition components that are accepted as part of
+ // the view definition: Prop, Events.
+ View(ExpoSelectableTextView::class) {
+ // Defines a setter for the `name` prop.
+ Prop("name") { view: ExpoSelectableTextView, prop: String ->
+ println(prop)
+ }
+ }
+ }
+}
diff --git a/modules/expo-selectable-text/android/src/main/java/expo/modules/selectabletext/ExpoSelectableTextView.kt b/modules/expo-selectable-text/android/src/main/java/expo/modules/selectabletext/ExpoSelectableTextView.kt
new file mode 100644
index 0000000000..5a13932613
--- /dev/null
+++ b/modules/expo-selectable-text/android/src/main/java/expo/modules/selectabletext/ExpoSelectableTextView.kt
@@ -0,0 +1,7 @@
+package expo.modules.selectabletext
+
+import android.content.Context
+import expo.modules.kotlin.AppContext
+import expo.modules.kotlin.views.ExpoView
+
+class ExpoSelectableTextView(context: Context, appContext: AppContext) : ExpoView(context, appContext)
diff --git a/modules/expo-selectable-text/expo-module.config.json b/modules/expo-selectable-text/expo-module.config.json
new file mode 100644
index 0000000000..f2ceb5a7a0
--- /dev/null
+++ b/modules/expo-selectable-text/expo-module.config.json
@@ -0,0 +1,9 @@
+{
+ "platforms": ["ios", "android", "web"],
+ "ios": {
+ "modules": ["ExpoSelectableTextModule"]
+ },
+ "android": {
+ "modules": ["expo.modules.selectabletext.ExpoSelectableTextModule"]
+ }
+}
diff --git a/modules/expo-selectable-text/index.ts b/modules/expo-selectable-text/index.ts
new file mode 100644
index 0000000000..5fe8d2e4bc
--- /dev/null
+++ b/modules/expo-selectable-text/index.ts
@@ -0,0 +1,26 @@
+import { NativeModulesProxy, EventEmitter, Subscription } from 'expo-modules-core';
+
+// 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('onChange', listener);
+}
+
+export { ExpoSelectableTextView, ExpoSelectableTextViewProps, ChangeEventPayload };
diff --git a/modules/expo-selectable-text/ios/ExpoProText.podspec b/modules/expo-selectable-text/ios/ExpoProText.podspec
new file mode 100644
index 0000000000..0564384ee1
--- /dev/null
+++ b/modules/expo-selectable-text/ios/ExpoProText.podspec
@@ -0,0 +1,21 @@
+Pod::Spec.new do |s|
+ s.name = 'ExpoProText'
+ s.version = '1.0.0'
+ s.summary = 'A sample project summary'
+ s.description = 'A sample project description'
+ s.author = ''
+ s.homepage = 'https://docs.expo.dev/modules/'
+ s.platform = :ios, '13.0'
+ s.source = { git: '' }
+ s.static_framework = true
+
+ s.dependency 'ExpoModulesCore'
+
+ # Swift/Objective-C compatibility
+ s.pod_target_xcconfig = {
+ 'DEFINES_MODULE' => 'YES',
+ 'SWIFT_COMPILATION_MODE' => 'wholemodule'
+ }
+
+ s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}"
+end
diff --git a/modules/expo-selectable-text/ios/ExpoProTextModule.swift b/modules/expo-selectable-text/ios/ExpoProTextModule.swift
new file mode 100644
index 0000000000..5f1fb95d92
--- /dev/null
+++ b/modules/expo-selectable-text/ios/ExpoProTextModule.swift
@@ -0,0 +1,26 @@
+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
+ }
+ }
+ }
+}
diff --git a/modules/expo-selectable-text/ios/ExpoProTextUtil.swift b/modules/expo-selectable-text/ios/ExpoProTextUtil.swift
new file mode 100644
index 0000000000..59936adc1f
--- /dev/null
+++ b/modules/expo-selectable-text/ios/ExpoProTextUtil.swift
@@ -0,0 +1,188 @@
+public class ExpoProTextUtil {
+ public static func hexToUIColor(hex: String?) -> UIColor {
+ guard let hex else {
+ return UIColor.black
+ }
+
+ let r, g, b, a: CGFloat
+
+ if hex.hasPrefix("#") {
+ let start = hex.index(hex.startIndex, offsetBy: 1)
+ let hexColor = String(hex[start...])
+
+ if hexColor.count == 8 {
+ let scanner = Scanner(string: hexColor)
+ var hexNumber: UInt64 = 0
+
+ if scanner.scanHexInt64(&hexNumber) {
+ r = CGFloat((hexNumber & 0xff000000) >> 24) / 255
+ g = CGFloat((hexNumber & 0x00ff0000) >> 16) / 255
+ b = CGFloat((hexNumber & 0x0000ff00) >> 8) / 255
+ a = CGFloat(hexNumber & 0x000000ff) / 255
+
+ return UIColor(red: r, green: g, blue: b, alpha: a)
+ }
+ } else if hexColor.count == 6 {
+ let scanner = Scanner(string: hexColor)
+ var hexNumber: UInt64 = 0
+
+ if scanner.scanHexInt64(&hexNumber) {
+ r = CGFloat((hexNumber & 0xff0000) >> 16) / 255
+ g = CGFloat((hexNumber & 0x00ff00) >> 8) / 255
+ b = CGFloat(hexNumber & 0x0000ff) / 255
+
+ return UIColor(red: r, green: g, blue: b, alpha: 1.0)
+ }
+ }
+ }
+
+ return UIColor.black
+ }
+}
+
+// https://github.com/WorldDownTown/CSS3ColorsSwift/blob/master/CSS3ColorsSwift/CSS3ColorsSwift.swift
+
+//
+// CSS3Colors.swift
+// CSS3Colors
+//
+// Created by Keisuke Shoji on 2016/11/07.
+// Copyright ยฉ 2016ๅนด Keisuke Shoji. All rights reserved.
+//
+
+import UIKit
+
+public extension UIColor {
+
+ class var whiteSmoke: UIColor { return #colorLiteral(red: 0.9607843137, green: 0.9607843137, blue: 0.9607843137, alpha: 1) } // F5F5F5
+ class var ghostWhite: UIColor { return #colorLiteral(red: 0.9725490196, green: 0.9725490196, blue: 1.0000000000, alpha: 1) } // F8F8FF
+ class var aliceBlue: UIColor { return #colorLiteral(red: 0.9411764706, green: 0.9725490196, blue: 1.0000000000, alpha: 1) } // F0F8FF
+ class var lavender: UIColor { return #colorLiteral(red: 0.9019607843, green: 0.9019607843, blue: 0.9803921569, alpha: 1) } // E6E6FA
+ class var azure: UIColor { return #colorLiteral(red: 0.9411764706, green: 1.0000000000, blue: 1.0000000000, alpha: 1) } // F0FFFF
+ class var lightCyan: UIColor { return #colorLiteral(red: 0.8784313725, green: 1.0000000000, blue: 1.0000000000, alpha: 1) } // E0FFFF
+ class var mintCream: UIColor { return #colorLiteral(red: 0.9607843137, green: 1.0000000000, blue: 0.9803921569, alpha: 1) } // F5FFFA
+ class var honeyDew: UIColor { return #colorLiteral(red: 0.9411764706, green: 1.0000000000, blue: 0.9411764706, alpha: 1) } // F0FFF0
+ class var ivory: UIColor { return #colorLiteral(red: 1.0000000000, green: 1.0000000000, blue: 0.9411764706, alpha: 1) } // FFFFF0
+ class var beige: UIColor { return #colorLiteral(red: 0.9607843137, green: 0.9607843137, blue: 0.8627450980, alpha: 1) } // F5F5DC
+ class var lightYellow: UIColor { return #colorLiteral(red: 1.0000000000, green: 1.0000000000, blue: 0.8784313725, alpha: 1) } // FFFFE0
+ class var lightGoldenRodYellow: UIColor { return #colorLiteral(red: 0.9803921569, green: 0.9803921569, blue: 0.8235294118, alpha: 1) } // FAFAD2
+ class var lemonChiffon: UIColor { return #colorLiteral(red: 1.0000000000, green: 0.9803921569, blue: 0.8039215686, alpha: 1) } // FFFACD
+ class var floralWhite: UIColor { return #colorLiteral(red: 1.0000000000, green: 0.9803921569, blue: 0.9411764706, alpha: 1) } // FFFAF0
+ class var oldLace: UIColor { return #colorLiteral(red: 0.9921568627, green: 0.9607843137, blue: 0.9019607843, alpha: 1) } // FDF5E6
+ class var cornSilk: UIColor { return #colorLiteral(red: 1.0000000000, green: 0.9725490196, blue: 0.8627450980, alpha: 1) } // FFF8DC
+ class var papayaWhip: UIColor { return #colorLiteral(red: 1.0000000000, green: 0.9372549020, blue: 0.8352941176, alpha: 1) } // FFEFD5
+ class var blanchedAlmond: UIColor { return #colorLiteral(red: 1.0000000000, green: 0.9215686275, blue: 0.8039215686, alpha: 1) } // FFEBCD
+ class var bisque: UIColor { return #colorLiteral(red: 1.0000000000, green: 0.8941176471, blue: 0.7686274510, alpha: 1) } // FFE4C4
+ class var snow: UIColor { return #colorLiteral(red: 1.0000000000, green: 0.9803921569, blue: 0.9803921569, alpha: 1) } // FFFAFA
+ class var linen: UIColor { return #colorLiteral(red: 0.9803921569, green: 0.9411764706, blue: 0.9019607843, alpha: 1) } // FAF0E6
+ class var antiqueWhite: UIColor { return #colorLiteral(red: 0.9803921569, green: 0.9215686275, blue: 0.8431372549, alpha: 1) } // FAEBD7
+ class var seaShell: UIColor { return #colorLiteral(red: 1.0000000000, green: 0.9607843137, blue: 0.9333333333, alpha: 1) } // FFF5EE
+ class var lavenderBlush: UIColor { return #colorLiteral(red: 1.0000000000, green: 0.9411764706, blue: 0.9607843137, alpha: 1) } // FFF0F5
+ class var mistyRose: UIColor { return #colorLiteral(red: 1.0000000000, green: 0.8941176471, blue: 0.8823529412, alpha: 1) } // FFE4E1
+ class var gainsboro: UIColor { return #colorLiteral(red: 0.8627450980, green: 0.8627450980, blue: 0.8627450980, alpha: 1) } // DCDCDC
+ class var lightGrayCSS3: UIColor { return #colorLiteral(red: 0.8274509804, green: 0.8274509804, blue: 0.8274509804, alpha: 1) } // D3D3D3
+ class var lightSteelBlue: UIColor { return #colorLiteral(red: 0.6901960784, green: 0.7686274510, blue: 0.8705882353, alpha: 1) } // B0C4DE
+ class var lightBlue: UIColor { return #colorLiteral(red: 0.6784313725, green: 0.8470588235, blue: 0.9019607843, alpha: 1) } // ADD8E6
+ class var lightSkyBlue: UIColor { return #colorLiteral(red: 0.5294117647, green: 0.8078431373, blue: 0.9803921569, alpha: 1) } // 87CEFA
+ class var powderBlue: UIColor { return #colorLiteral(red: 0.6901960784, green: 0.8784313725, blue: 0.9019607843, alpha: 1) } // B0E0E6
+ class var paleTurquoise: UIColor { return #colorLiteral(red: 0.6862745098, green: 0.9333333333, blue: 0.9333333333, alpha: 1) } // AFEEEE
+ class var skyBlue: UIColor { return #colorLiteral(red: 0.5294117647, green: 0.8078431373, blue: 0.9215686275, alpha: 1) } // 87CEEB
+ class var mediumAquamarine: UIColor { return #colorLiteral(red: 0.4000000000, green: 0.8039215686, blue: 0.6666666667, alpha: 1) } // 66CDAA
+ class var aquamarine: UIColor { return #colorLiteral(red: 0.4980392157, green: 1.0000000000, blue: 0.8313725490, alpha: 1) } // 7FFFD4
+ class var paleGreen: UIColor { return #colorLiteral(red: 0.5960784314, green: 0.9843137255, blue: 0.5960784314, alpha: 1) } // 98FB98
+ class var lightGreen: UIColor { return #colorLiteral(red: 0.5647058824, green: 0.9333333333, blue: 0.5647058824, alpha: 1) } // 90EE90
+ class var khaki: UIColor { return #colorLiteral(red: 0.9411764706, green: 0.9019607843, blue: 0.5490196078, alpha: 1) } // F0E68C
+ class var paleGoldenRod: UIColor { return #colorLiteral(red: 0.9333333333, green: 0.9098039216, blue: 0.6666666667, alpha: 1) } // EEE8AA
+ class var moccasin: UIColor { return #colorLiteral(red: 1.0000000000, green: 0.8941176471, blue: 0.7098039216, alpha: 1) } // FFE4B5
+ class var navajoWhite: UIColor { return #colorLiteral(red: 1.0000000000, green: 0.8705882353, blue: 0.6784313725, alpha: 1) } // FFDEAD
+ class var peachPuff: UIColor { return #colorLiteral(red: 1.0000000000, green: 0.8549019608, blue: 0.7254901961, alpha: 1) } // FFDAB9
+ class var wheat: UIColor { return #colorLiteral(red: 0.9607843137, green: 0.8705882353, blue: 0.7019607843, alpha: 1) } // F5DEB3
+ class var pink: UIColor { return #colorLiteral(red: 1.0000000000, green: 0.7529411765, blue: 0.7960784314, alpha: 1) } // FFC0CB
+ class var lightPink: UIColor { return #colorLiteral(red: 1.0000000000, green: 0.7137254902, blue: 0.7568627451, alpha: 1) } // FFB6C1
+ class var thistle: UIColor { return #colorLiteral(red: 0.8470588235, green: 0.7490196078, blue: 0.8470588235, alpha: 1) } // D8BFD8
+ class var plum: UIColor { return #colorLiteral(red: 0.8666666667, green: 0.6274509804, blue: 0.8666666667, alpha: 1) } // DDA0DD
+ class var silver: UIColor { return #colorLiteral(red: 0.7529411765, green: 0.7529411765, blue: 0.7529411765, alpha: 1) } // C0C0C0
+ class var darkGrayCSS3: UIColor { return #colorLiteral(red: 0.6627450980, green: 0.6627450980, blue: 0.6627450980, alpha: 1) } // A9A9A9
+ class var lightSlateGray: UIColor { return #colorLiteral(red: 0.4666666667, green: 0.5333333333, blue: 0.6000000000, alpha: 1) } // 778899
+ class var slateGray: UIColor { return #colorLiteral(red: 0.4392156863, green: 0.5019607843, blue: 0.5647058824, alpha: 1) } // 708090
+ class var slateBlue: UIColor { return #colorLiteral(red: 0.4156862745, green: 0.3529411765, blue: 0.8039215686, alpha: 1) } // 6A5ACD
+ class var steelBlue: UIColor { return #colorLiteral(red: 0.2745098039, green: 0.5098039216, blue: 0.7058823529, alpha: 1) } // 4682B4
+ class var mediumSlateBlue: UIColor { return #colorLiteral(red: 0.4823529412, green: 0.4078431373, blue: 0.9333333333, alpha: 1) } // 7B68EE
+ class var royalBlue: UIColor { return #colorLiteral(red: 0.2549019608, green: 0.4117647059, blue: 0.8823529412, alpha: 1) } // 4169E1
+ class var dodgerBlue: UIColor { return #colorLiteral(red: 0.1176470588, green: 0.5647058824, blue: 1.0000000000, alpha: 1) } // 1E90FF
+ class var cornflowerBlue: UIColor { return #colorLiteral(red: 0.3921568627, green: 0.5843137255, blue: 0.9294117647, alpha: 1) } // 6495ED
+ class var deepSkyBlue: UIColor { return #colorLiteral(red: 0.0000000000, green: 0.7490196078, blue: 1.0000000000, alpha: 1) } // 00BFFF
+ class var aqua: UIColor { return #colorLiteral(red: 0.0000000000, green: 1.0000000000, blue: 1.0000000000, alpha: 1) } // 00FFFF
+ class var turquoise: UIColor { return #colorLiteral(red: 0.2509803922, green: 0.8784313725, blue: 0.8156862745, alpha: 1) } // 40E0D0
+ class var mediumTurquoise: UIColor { return #colorLiteral(red: 0.2823529412, green: 0.8196078431, blue: 0.8000000000, alpha: 1) } // 48D1CC
+ class var darkTurquoise: UIColor { return #colorLiteral(red: 0.0000000000, green: 0.8078431373, blue: 0.8196078431, alpha: 1) } // 00CED1
+ class var lightSeaGreen: UIColor { return #colorLiteral(red: 0.1254901961, green: 0.6980392157, blue: 0.6666666667, alpha: 1) } // 20B2AA
+ class var mediumSpringGreen: UIColor { return #colorLiteral(red: 0.0000000000, green: 0.9803921569, blue: 0.6039215686, alpha: 1) } // 00FA9A
+ class var springGreen: UIColor { return #colorLiteral(red: 0.0000000000, green: 1.0000000000, blue: 0.4980392157, alpha: 1) } // 00FF7F
+ class var lime: UIColor { return #colorLiteral(red: 0.0000000000, green: 1.0000000000, blue: 0.0000000000, alpha: 1) } // 00FF00
+ class var limeGreen: UIColor { return #colorLiteral(red: 0.1960784314, green: 0.8039215686, blue: 0.1960784314, alpha: 1) } // 32CD32
+ class var yellowGreen: UIColor { return #colorLiteral(red: 0.6039215686, green: 0.8039215686, blue: 0.1960784314, alpha: 1) } // 9ACD32
+ class var lawnGreen: UIColor { return #colorLiteral(red: 0.4862745098, green: 0.9882352941, blue: 0.0000000000, alpha: 1) } // 7CFC00
+ class var chartreuse: UIColor { return #colorLiteral(red: 0.4980392157, green: 1.0000000000, blue: 0.0000000000, alpha: 1) } // 7FFF00
+ class var greenYellow: UIColor { return #colorLiteral(red: 0.6784313725, green: 1.0000000000, blue: 0.1843137255, alpha: 1) } // ADFF2F
+ class var gold: UIColor { return #colorLiteral(red: 1.0000000000, green: 0.8431372549, blue: 0.0000000000, alpha: 1) } // FFD700
+ class var orangeCSS3: UIColor { return #colorLiteral(red: 1.0000000000, green: 0.6470588235, blue: 0.0000000000, alpha: 1) } // FFA500
+ class var darkOrange: UIColor { return #colorLiteral(red: 1.0000000000, green: 0.5490196078, blue: 0.0000000000, alpha: 1) } // FF8C00
+ class var goldenRod: UIColor { return #colorLiteral(red: 0.8549019608, green: 0.6470588235, blue: 0.1254901961, alpha: 1) } // DAA520
+ class var burlyWood: UIColor { return #colorLiteral(red: 0.8705882353, green: 0.7215686275, blue: 0.5294117647, alpha: 1) } // DEB887
+ class var tan: UIColor { return #colorLiteral(red: 0.8235294118, green: 0.7058823529, blue: 0.5490196078, alpha: 1) } // D2B48C
+ class var sandyBrown: UIColor { return #colorLiteral(red: 0.9568627451, green: 0.6431372549, blue: 0.3764705882, alpha: 1) } // F4A460
+ class var darkSalmon: UIColor { return #colorLiteral(red: 0.9137254902, green: 0.5882352941, blue: 0.4784313725, alpha: 1) } // E9967A
+ class var lightCoral: UIColor { return #colorLiteral(red: 0.9411764706, green: 0.5019607843, blue: 0.5019607843, alpha: 1) } // F08080
+ class var salmon: UIColor { return #colorLiteral(red: 0.9803921569, green: 0.5019607843, blue: 0.4470588235, alpha: 1) } // FA8072
+ class var lightSalmon: UIColor { return #colorLiteral(red: 1.0000000000, green: 0.6274509804, blue: 0.4784313725, alpha: 1) } // FFA07A
+ class var coral: UIColor { return #colorLiteral(red: 1.0000000000, green: 0.4980392157, blue: 0.3137254902, alpha: 1) } // FF7F50
+ class var tomato: UIColor { return #colorLiteral(red: 1.0000000000, green: 0.3882352941, blue: 0.2784313725, alpha: 1) } // FF6347
+ class var orangeRed: UIColor { return #colorLiteral(red: 1.0000000000, green: 0.2705882353, blue: 0.0000000000, alpha: 1) } // FF4500
+ class var deepPink: UIColor { return #colorLiteral(red: 1.0000000000, green: 0.0784313725, blue: 0.5764705882, alpha: 1) } // FF1493
+ class var hotPink: UIColor { return #colorLiteral(red: 1.0000000000, green: 0.4117647059, blue: 0.7058823529, alpha: 1) } // FF69B4
+ class var paleVioletRed: UIColor { return #colorLiteral(red: 0.8470588235, green: 0.4392156863, blue: 0.5764705882, alpha: 1) } // D87093
+ class var violet: UIColor { return #colorLiteral(red: 0.9333333333, green: 0.5098039216, blue: 0.9333333333, alpha: 1) } // EE82EE
+ class var orchid: UIColor { return #colorLiteral(red: 0.8549019608, green: 0.4392156863, blue: 0.8392156863, alpha: 1) } // DA70D6
+ class var fuchsia: UIColor { return #colorLiteral(red: 1.0000000000, green: 0.0000000000, blue: 1.0000000000, alpha: 1) } // FF00FF
+ class var mediumOrchid: UIColor { return #colorLiteral(red: 0.7294117647, green: 0.3333333333, blue: 0.8274509804, alpha: 1) } // BA55D3
+ class var darkOrchid: UIColor { return #colorLiteral(red: 0.6000000000, green: 0.1960784314, blue: 0.8000000000, alpha: 1) } // 9932CC
+ class var darkViolet: UIColor { return #colorLiteral(red: 0.5803921569, green: 0.0000000000, blue: 0.8274509804, alpha: 1) } // 9400D3
+ class var blueViolet: UIColor { return #colorLiteral(red: 0.5411764706, green: 0.1686274510, blue: 0.8862745098, alpha: 1) } // 8A2BE2
+ class var mediumPurple: UIColor { return #colorLiteral(red: 0.5764705882, green: 0.4392156863, blue: 0.8470588235, alpha: 1) } // 9370D8
+ class var mediumBlue: UIColor { return #colorLiteral(red: 0.0000000000, green: 0.0000000000, blue: 0.8039215686, alpha: 1) } // 0000CD
+ class var darkCyan: UIColor { return #colorLiteral(red: 0.0000000000, green: 0.5450980392, blue: 0.5450980392, alpha: 1) } // 008B8B
+ class var cadetBlue: UIColor { return #colorLiteral(red: 0.3725490196, green: 0.6196078431, blue: 0.6274509804, alpha: 1) } // 5F9EA0
+ class var darkSeaGreen: UIColor { return #colorLiteral(red: 0.5607843137, green: 0.7372549020, blue: 0.5607843137, alpha: 1) } // 8FBC8F
+ class var mediumSeaGreen: UIColor { return #colorLiteral(red: 0.2352941176, green: 0.7019607843, blue: 0.4431372549, alpha: 1) } // 3CB371
+ class var teal: UIColor { return #colorLiteral(red: 0.0000000000, green: 0.5019607843, blue: 0.5019607843, alpha: 1) } // 008080
+ class var forestGreen: UIColor { return #colorLiteral(red: 0.1333333333, green: 0.5450980392, blue: 0.1333333333, alpha: 1) } // 228B22
+ class var seaGreen: UIColor { return #colorLiteral(red: 0.1803921569, green: 0.5450980392, blue: 0.3411764706, alpha: 1) } // 2E8B57
+ class var darkKhaki: UIColor { return #colorLiteral(red: 0.7411764706, green: 0.7176470588, blue: 0.4196078431, alpha: 1) } // BDB76B
+ class var peru: UIColor { return #colorLiteral(red: 0.8039215686, green: 0.5215686275, blue: 0.2470588235, alpha: 1) } // CD853F
+ class var crimson: UIColor { return #colorLiteral(red: 0.8627450980, green: 0.0784313725, blue: 0.2352941176, alpha: 1) } // DC143C
+ class var indianRed: UIColor { return #colorLiteral(red: 0.8039215686, green: 0.3607843137, blue: 0.3607843137, alpha: 1) } // CD5C5C
+ class var rosyBrown: UIColor { return #colorLiteral(red: 0.7372549020, green: 0.5607843137, blue: 0.5607843137, alpha: 1) } // BC8F8F
+ class var mediumVioletRed: UIColor { return #colorLiteral(red: 0.7803921569, green: 0.0823529412, blue: 0.5215686275, alpha: 1) } // C71585
+ class var dimGray: UIColor { return #colorLiteral(red: 0.4117647059, green: 0.4117647059, blue: 0.4117647059, alpha: 1) } // 696969
+ class var midnightBlue: UIColor { return #colorLiteral(red: 0.0980392157, green: 0.0980392157, blue: 0.4392156863, alpha: 1) } // 191970
+ class var darkSlateBlue: UIColor { return #colorLiteral(red: 0.2823529412, green: 0.2392156863, blue: 0.5450980392, alpha: 1) } // 483D8B
+ class var darkBlue: UIColor { return #colorLiteral(red: 0.0000000000, green: 0.0000000000, blue: 0.5450980392, alpha: 1) } // 00008B
+ class var navy: UIColor { return #colorLiteral(red: 0.0000000000, green: 0.0000000000, blue: 0.5019607843, alpha: 1) } // 000080
+ class var darkSlateGray: UIColor { return #colorLiteral(red: 0.1843137255, green: 0.3098039216, blue: 0.3098039216, alpha: 1) } // 2F4F4F
+ class var greenCSS3: UIColor { return #colorLiteral(red: 0.0000000000, green: 0.5019607843, blue: 0.0000000000, alpha: 1) } // 008000
+ class var darkGreen: UIColor { return #colorLiteral(red: 0.0000000000, green: 0.3921568627, blue: 0.0000000000, alpha: 1) } // 006400
+ class var darkOliveGreen: UIColor { return #colorLiteral(red: 0.3333333333, green: 0.4196078431, blue: 0.1843137255, alpha: 1) } // 556B2F
+ class var oliveDrab: UIColor { return #colorLiteral(red: 0.4196078431, green: 0.5568627451, blue: 0.1372549020, alpha: 1) } // 6B8E23
+ class var olive: UIColor { return #colorLiteral(red: 0.5019607843, green: 0.5019607843, blue: 0.0000000000, alpha: 1) } // 808000
+ class var darkGoldenRod: UIColor { return #colorLiteral(red: 0.7215686275, green: 0.5254901961, blue: 0.0431372549, alpha: 1) } // B8860B
+ class var chocolate: UIColor { return #colorLiteral(red: 0.8235294118, green: 0.4117647059, blue: 0.1176470588, alpha: 1) } // D2691E
+ class var sienna: UIColor { return #colorLiteral(red: 0.6274509804, green: 0.3215686275, blue: 0.1764705882, alpha: 1) } // A0522D
+ class var saddleBrown: UIColor { return #colorLiteral(red: 0.5450980392, green: 0.2705882353, blue: 0.0745098039, alpha: 1) } // 8B4513
+ class var fireBrick: UIColor { return #colorLiteral(red: 0.6980392157, green: 0.1333333333, blue: 0.1333333333, alpha: 1) } // B22222
+ class var brownCSS3: UIColor { return #colorLiteral(red: 0.6470588235, green: 0.1647058824, blue: 0.1647058824, alpha: 1) } // A52A2A
+ class var maroon: UIColor { return #colorLiteral(red: 0.5019607843, green: 0.0000000000, blue: 0.0000000000, alpha: 1) } // 800000
+ class var darkRed: UIColor { return #colorLiteral(red: 0.5450980392, green: 0.0000000000, blue: 0.0000000000, alpha: 1) } // 8B0000
+ class var darkMagenta: UIColor { return #colorLiteral(red: 0.5450980392, green: 0.0000000000, blue: 0.5450980392, alpha: 1) } // 8B008B
+ class var indigo: UIColor { return #colorLiteral(red: 0.2941176471, green: 0.0000000000, blue: 0.5098039216, alpha: 1) } // 4B0082
+}
diff --git a/modules/expo-selectable-text/ios/ExpoProTextView.swift b/modules/expo-selectable-text/ios/ExpoProTextView.swift
new file mode 100644
index 0000000000..25ae0a74cb
--- /dev/null
+++ b/modules/expo-selectable-text/ios/ExpoProTextView.swift
@@ -0,0 +1,136 @@
+import Foundation
+import ExpoModulesCore
+
+class ExpoProTextView: ExpoView {
+ let textView: UITextView = UITextView()
+ var segments: Array = [] {
+ didSet {
+ setText()
+ }
+ }
+
+ let onTextPress = EventDispatcher()
+ let onTextLongPress = EventDispatcher()
+ let onTextLayout = EventDispatcher()
+
+ public required init(appContext: AppContext? = nil) {
+ super.init(appContext: appContext)
+
+ // Configure default appearance
+ textView.scrollsToTop = false
+ textView.isEditable = false
+ textView.isScrollEnabled = false
+ textView.backgroundColor = .clear
+
+ // Add the text view to the root view
+ addSubview(textView)
+
+ // Configure the press recognizer
+ configurePressRecognizer()
+ }
+
+ override func layoutSubviews() {
+ // Set the textView's frame on layout
+ setSize()
+ }
+
+ func configurePressRecognizer() -> Void {
+ let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(callOnPress(_:)))
+ textView.addGestureRecognizer(tapGestureRecognizer)
+ }
+
+ @IBAction func callOnPress(_ sender: UITapGestureRecognizer) -> Void {
+ if let segment = getPressedSegment(sender) {
+ if segment.handlePress, textView.selectedTextRange == nil {
+ onTextPress([
+ "index": segment.index
+ ])
+ } else {
+ // Clear the selected text range if we are not pressing on a link
+ textView.selectedTextRange = nil
+ }
+ }
+ }
+
+ func setSize() -> Void {
+ // Figure out the height of our text and create a CGRect
+ let maxWidth = bounds.width
+ let sizeThatFits = textView.sizeThatFits(CGSize(width: maxWidth, height: CGFloat(MAXFLOAT)))
+ let size = CGSize(width: maxWidth, height: sizeThatFits.height)
+ textView.frame.size = size
+
+ onTextLayout([
+ "height": sizeThatFits.height,
+ "width": maxWidth
+ ])
+ }
+
+ func setText() -> Void {
+ let finalAttributedString = NSMutableAttributedString()
+
+ 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),
+ ]
+
+ // Create the attributed string with the generic attributes
+ let string = NSMutableAttributedString(string: segment.text, attributes: attributes)
+
+ // Set the paragraph style attributes if necessary
+ if let lineHeight = segment.style?.lineHeight {
+ let paragraphStyle = NSMutableParagraphStyle()
+ paragraphStyle.lineSpacing = lineHeight
+ string.addAttribute(NSAttributedString.Key.paragraphStyle, value: paragraphStyle, range: NSMakeRange(0, string.length))
+ }
+
+ if let textDecorationLine = segment.style?.textDecorationLine {
+ if textDecorationLine == .underline || textDecorationLine == .underlineLineThrough {
+ string.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue, range: NSMakeRange(0, string.length))
+ }
+
+ if textDecorationLine == .lineThrough || textDecorationLine == .underlineLineThrough {
+ string.addAttribute(NSAttributedString.Key.strikethroughStyle, value: NSUnderlineStyle.single.rawValue, range: NSMakeRange(0, string.length))
+ }
+ }
+
+ finalAttributedString.append(string)
+ }
+
+ textView.attributedText = finalAttributedString
+
+ self.setNeedsLayout()
+ }
+
+ func getPressedSegment(_ sender: UITapGestureRecognizer) -> TextSegment? {
+ 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)
+
+ let text = textView.attributedText.string
+ var foundSegment: TextSegment?
+
+ // Check each segment
+ segments.forEach { segment in
+ let range = text.range(of: segment.text)
+ // Figure out the bounds
+ if let lowerBound = range?.lowerBound, let upperBound = range?.upperBound {
+ let lowerIndex = text.distance(from: text.startIndex , to: lowerBound)
+ let upperIndex = text.distance(from: text.startIndex, to: upperBound)
+
+ if charIndex >= lowerIndex, charIndex <= upperIndex {
+ foundSegment = segment
+ }
+ }
+ }
+
+ return foundSegment
+ }
+}
diff --git a/modules/expo-selectable-text/ios/TextSegment.swift b/modules/expo-selectable-text/ios/TextSegment.swift
new file mode 100644
index 0000000000..8c410aaaab
--- /dev/null
+++ b/modules/expo-selectable-text/ios/TextSegment.swift
@@ -0,0 +1,13 @@
+import ExpoModulesCore
+
+struct TextSegments: Decodable {
+ let segments: Array
+}
+
+struct TextSegment: Decodable {
+ let index: Int
+ let text: String
+ let style: TextStyle?
+ let handlePress: Bool
+ let handleLongPress: Bool
+}
diff --git a/modules/expo-selectable-text/ios/TextStyle.swift b/modules/expo-selectable-text/ios/TextStyle.swift
new file mode 100644
index 0000000000..4de16d4a75
--- /dev/null
+++ b/modules/expo-selectable-text/ios/TextStyle.swift
@@ -0,0 +1,31 @@
+struct TextStyle: Decodable {
+ var color: String? = "black"
+ var fontSize: CGFloat? = 12
+ var fontStyle: String? = "normal"
+ var fontWeight: FontWeight?
+ var letterSpacing: Double?
+ var textAlign: String? = "auto"
+ var lineHeight: Double?
+ var textDecorationLine: TextDecorationLine?
+}
+
+enum FontWeight: String, Decodable {
+ case bold
+ case normal
+
+ func toFontWeight() -> UIFont.Weight {
+ switch self {
+ case .bold:
+ return .bold
+ case .normal:
+ return .regular
+ }
+ }
+}
+
+enum TextDecorationLine: String, Decodable {
+ case underline
+ case lineThrough = "line-through"
+ case underlineLineThrough = "underline line-through"
+ case normal
+}
diff --git a/modules/expo-selectable-text/src/ExpoProText.types.ts b/modules/expo-selectable-text/src/ExpoProText.types.ts
new file mode 100644
index 0000000000..80fa156651
--- /dev/null
+++ b/modules/expo-selectable-text/src/ExpoProText.types.ts
@@ -0,0 +1,52 @@
+import React from 'react'
+import { ColorValue } from 'react-native'
+
+interface ExpoProTextViewCommonProps {
+ selectable?: boolean
+}
+
+export interface ExpoProTextViewProps extends ExpoProTextViewCommonProps {
+ children: React.ReactNode
+ style?: ExpoProTextStyle
+ onPress?: () => void
+ onLongPress?: () => void
+}
+
+export interface ExpoProTextNativeViewProps extends ExpoProTextViewCommonProps {
+ segments: string
+ textStyle?: ExpoProTextStyle
+ onTextPress?: (event: ExpoProTextPressEvent) => void
+ onTextLongPress?: (event: ExpoProTextPressEvent) => void
+ onTextLayout?: (event: ExpoProTextLayoutEvent) => void
+ disableLongPress: boolean
+}
+
+interface ExpoProTextStyle {
+ color: ColorValue
+ fontSize?: number
+ fontStyle?: 'normal' | 'italic'
+ fontWeight?: 'normal' | 'bold'
+ letterSpacing?: number
+ textAlign?: 'auto' | 'left' | 'right' | 'center' | 'justify'
+ lineHeight?: number
+}
+
+export interface ExpoProTextLayoutEvent {
+ nativeEvent: {
+ height: number
+ }
+}
+
+export interface ExpoProTextPressEvent {
+ nativeEvent: {
+ index: number
+ }
+}
+
+export interface ExpoProTextSegment {
+ index: number
+ text: string
+ style?: ExpoProTextStyle
+ handlePress?: boolean
+ handleLongPress?: boolean
+}
diff --git a/modules/expo-selectable-text/src/ExpoProTextModule.ts b/modules/expo-selectable-text/src/ExpoProTextModule.ts
new file mode 100644
index 0000000000..541b6700a5
--- /dev/null
+++ b/modules/expo-selectable-text/src/ExpoProTextModule.ts
@@ -0,0 +1,5 @@
+import { requireNativeModule } from 'expo-modules-core';
+
+// It loads the native module object from the JSI or falls back to
+// the bridge module (from NativeModulesProxy) if the remote debugger is on.
+export default requireNativeModule('ExpoProText');
diff --git a/modules/expo-selectable-text/src/ExpoProTextModule.web.ts b/modules/expo-selectable-text/src/ExpoProTextModule.web.ts
new file mode 100644
index 0000000000..be76495bbf
--- /dev/null
+++ b/modules/expo-selectable-text/src/ExpoProTextModule.web.ts
@@ -0,0 +1,13 @@
+import { EventEmitter } from 'expo-modules-core';
+
+const emitter = new EventEmitter({} as any);
+
+export default {
+ PI: Math.PI,
+ async setValueAsync(value: string): Promise {
+ emitter.emit('onChange', { value });
+ },
+ hello() {
+ return 'Hello world! ๐';
+ },
+};
diff --git a/modules/expo-selectable-text/src/ExpoProTextView.tsx b/modules/expo-selectable-text/src/ExpoProTextView.tsx
new file mode 100644
index 0000000000..e800ffafd4
--- /dev/null
+++ b/modules/expo-selectable-text/src/ExpoProTextView.tsx
@@ -0,0 +1,109 @@
+import { requireNativeViewManager } from 'expo-modules-core'
+import * as React from 'react'
+
+import {
+ ExpoProTextLayoutEvent,
+ ExpoProTextNativeViewProps,
+ ExpoProTextPressEvent,
+ ExpoProTextSegment,
+ ExpoProTextViewProps,
+} from './ExpoProText.types'
+import { Text, View } from 'react-native'
+
+const NativeView: React.ComponentType =
+ requireNativeViewManager('ExpoProText')
+
+export default function ExpoProTextView({
+ style,
+ children,
+ selectable = false,
+ onPress,
+ onLongPress,
+}: ExpoProTextViewProps) {
+ const [dims, setDims] = React.useState({ height: 0 })
+ const segmentPressCallbacks = React.useRef<
+ Array<{ index: number; onPress: () => void }>
+ >([])
+ const segmentLongPressCallbacks = React.useRef<
+ Array<{ index: number, onLongPress: () => void, }>
+ >([])
+
+ const onTextLayout = React.useCallback((e: ExpoProTextLayoutEvent) => {
+ 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,
+ )
+ onPressSegment?.onPress()
+ }, [])
+
+ const textSegments = React.useMemo(() => {
+ const segments: ExpoProTextSegment[] = []
+
+ for (const [index, child] of React.Children.toArray(children).entries()) {
+ if (typeof child === 'string') {
+ segments.push({
+ index,
+ text: child,
+ style,
+ handlePress: onPress !== undefined,
+ handleLongPress: onLongPress !== undefined,
+ })
+ } else if (
+ React.isValidElement(child) &&
+ (child as React.ReactElement).type === Text
+ ) {
+ const { onPress, onLongPress, children, style } = child.props
+
+ segments.push({
+ index,
+ text: children,
+ style,
+ handlePress: onPress !== undefined,
+ handleLongPress: onLongPress !== undefined,
+ })
+
+ if (onPress !== undefined) {
+ segmentPressCallbacks.current.push({
+ index,
+ onPress,
+ })
+ }
+
+ if (onLongPress !== undefined) {
+ segmentLongPressCallbacks.current.push({
+ index,
+ onLongPress,
+ })
+ }
+ }
+ }
+
+ return segments
+ }, [children])
+
+ const segmentsJson = React.useMemo(() => {
+ const json = JSON.stringify({ segments: textSegments })
+ console.log(json)
+ return json
+ }, [textSegments])
+
+ return (
+
+
+
+ )
+}
diff --git a/modules/expo-selectable-text/src/ExpoProTextView.web.tsx b/modules/expo-selectable-text/src/ExpoProTextView.web.tsx
new file mode 100644
index 0000000000..f3319886ae
--- /dev/null
+++ b/modules/expo-selectable-text/src/ExpoProTextView.web.tsx
@@ -0,0 +1,11 @@
+import * as React from 'react'
+
+import { ExpoProTextViewProps } from './ExpoProText.types'
+
+export default function ExpoProTextView(props: ExpoProTextViewProps) {
+ return (
+
+ {props.name}
+
+ )
+}