react-native-enriched 0.0.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +875 -0
  3. package/ReactNativeEnriched.podspec +27 -0
  4. package/android/build.gradle +101 -0
  5. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedTextInputViewManagerDelegate.java +146 -0
  6. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedTextInputViewManagerInterface.java +55 -0
  7. package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/ComponentDescriptors.cpp +22 -0
  8. package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/ComponentDescriptors.h +24 -0
  9. package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/EventEmitters.cpp +118 -0
  10. package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/EventEmitters.h +95 -0
  11. package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/Props.cpp +128 -0
  12. package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/Props.h +577 -0
  13. package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/ShadowNodes.cpp +17 -0
  14. package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/ShadowNodes.h +23 -0
  15. package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/States.cpp +16 -0
  16. package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/States.h +20 -0
  17. package/android/gradle.properties +5 -0
  18. package/android/src/main/AndroidManifest.xml +3 -0
  19. package/android/src/main/AndroidManifestNew.xml +2 -0
  20. package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputView.kt +535 -0
  21. package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewLayoutManager.kt +64 -0
  22. package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewManager.kt +292 -0
  23. package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewPackage.kt +19 -0
  24. package/android/src/main/java/com/swmansion/enriched/events/MentionHandler.kt +40 -0
  25. package/android/src/main/java/com/swmansion/enriched/events/OnChangeHtmlEvent.kt +28 -0
  26. package/android/src/main/java/com/swmansion/enriched/events/OnChangeSelectionEvent.kt +29 -0
  27. package/android/src/main/java/com/swmansion/enriched/events/OnChangeStateEvent.kt +24 -0
  28. package/android/src/main/java/com/swmansion/enriched/events/OnChangeTextEvent.kt +30 -0
  29. package/android/src/main/java/com/swmansion/enriched/events/OnInputBlurEvent.kt +27 -0
  30. package/android/src/main/java/com/swmansion/enriched/events/OnInputFocusEvent.kt +27 -0
  31. package/android/src/main/java/com/swmansion/enriched/events/OnLinkDetectedEvent.kt +30 -0
  32. package/android/src/main/java/com/swmansion/enriched/events/OnMentionDetectedEvent.kt +29 -0
  33. package/android/src/main/java/com/swmansion/enriched/events/OnMentionEvent.kt +33 -0
  34. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedBlockQuoteSpan.kt +34 -0
  35. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedBoldSpan.kt +10 -0
  36. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedCodeBlockSpan.kt +38 -0
  37. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedH1Span.kt +17 -0
  38. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedH2Span.kt +17 -0
  39. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedH3Span.kt +17 -0
  40. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedImageSpan.kt +41 -0
  41. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedInlineCodeSpan.kt +16 -0
  42. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedItalicSpan.kt +10 -0
  43. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedLinkSpan.kt +24 -0
  44. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedMentionSpan.kt +36 -0
  45. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedOrderedListSpan.kt +71 -0
  46. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedSpans.kt +111 -0
  47. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedStrikeThroughSpan.kt +9 -0
  48. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedUnderlineSpan.kt +9 -0
  49. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedUnorderedListSpan.kt +49 -0
  50. package/android/src/main/java/com/swmansion/enriched/spans/interfaces/EnrichedBlockSpan.kt +4 -0
  51. package/android/src/main/java/com/swmansion/enriched/spans/interfaces/EnrichedHeadingSpan.kt +4 -0
  52. package/android/src/main/java/com/swmansion/enriched/spans/interfaces/EnrichedInlineSpan.kt +4 -0
  53. package/android/src/main/java/com/swmansion/enriched/spans/interfaces/EnrichedParagraphSpan.kt +4 -0
  54. package/android/src/main/java/com/swmansion/enriched/spans/interfaces/EnrichedSpan.kt +4 -0
  55. package/android/src/main/java/com/swmansion/enriched/spans/interfaces/EnrichedZeroWidthSpaceSpan.kt +5 -0
  56. package/android/src/main/java/com/swmansion/enriched/styles/HtmlStyle.kt +227 -0
  57. package/android/src/main/java/com/swmansion/enriched/styles/InlineStyles.kt +146 -0
  58. package/android/src/main/java/com/swmansion/enriched/styles/ListStyles.kt +173 -0
  59. package/android/src/main/java/com/swmansion/enriched/styles/ParagraphStyles.kt +186 -0
  60. package/android/src/main/java/com/swmansion/enriched/styles/ParametrizedStyles.kt +223 -0
  61. package/android/src/main/java/com/swmansion/enriched/utils/EnrichedParser.java +857 -0
  62. package/android/src/main/java/com/swmansion/enriched/utils/EnrichedSelection.kt +285 -0
  63. package/android/src/main/java/com/swmansion/enriched/utils/EnrichedSpanState.kt +204 -0
  64. package/android/src/main/java/com/swmansion/enriched/utils/Utils.kt +91 -0
  65. package/android/src/main/java/com/swmansion/enriched/watchers/EnrichedSpanWatcher.kt +73 -0
  66. package/android/src/main/java/com/swmansion/enriched/watchers/EnrichedTextWatcher.kt +51 -0
  67. package/android/src/main/new_arch/CMakeLists.txt +56 -0
  68. package/android/src/main/new_arch/RNEnrichedTextInputViewSpec.cpp +22 -0
  69. package/android/src/main/new_arch/RNEnrichedTextInputViewSpec.h +26 -0
  70. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputComponentDescriptor.h +35 -0
  71. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputMeasurementManager.cpp +51 -0
  72. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputMeasurementManager.h +26 -0
  73. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputShadowNode.cpp +34 -0
  74. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputShadowNode.h +54 -0
  75. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputState.cpp +9 -0
  76. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputState.h +25 -0
  77. package/ios/EnrichedTextInputView.h +33 -0
  78. package/ios/EnrichedTextInputView.mm +1190 -0
  79. package/ios/EnrichedTextInputViewManager.mm +13 -0
  80. package/ios/config/InputConfig.h +67 -0
  81. package/ios/config/InputConfig.mm +382 -0
  82. package/ios/generated/RNEnrichedTextInputViewSpec/ComponentDescriptors.cpp +22 -0
  83. package/ios/generated/RNEnrichedTextInputViewSpec/ComponentDescriptors.h +24 -0
  84. package/ios/generated/RNEnrichedTextInputViewSpec/EventEmitters.cpp +118 -0
  85. package/ios/generated/RNEnrichedTextInputViewSpec/EventEmitters.h +95 -0
  86. package/ios/generated/RNEnrichedTextInputViewSpec/Props.cpp +128 -0
  87. package/ios/generated/RNEnrichedTextInputViewSpec/Props.h +577 -0
  88. package/ios/generated/RNEnrichedTextInputViewSpec/RCTComponentViewHelpers.h +384 -0
  89. package/ios/generated/RNEnrichedTextInputViewSpec/ShadowNodes.cpp +17 -0
  90. package/ios/generated/RNEnrichedTextInputViewSpec/ShadowNodes.h +23 -0
  91. package/ios/generated/RNEnrichedTextInputViewSpec/States.cpp +16 -0
  92. package/ios/generated/RNEnrichedTextInputViewSpec/States.h +20 -0
  93. package/ios/inputParser/InputParser.h +11 -0
  94. package/ios/inputParser/InputParser.mm +659 -0
  95. package/ios/inputTextView/InputTextView.h +6 -0
  96. package/ios/inputTextView/InputTextView.mm +115 -0
  97. package/ios/internals/EnrichedTextInputViewComponentDescriptor.h +17 -0
  98. package/ios/internals/EnrichedTextInputViewShadowNode.h +40 -0
  99. package/ios/internals/EnrichedTextInputViewShadowNode.mm +83 -0
  100. package/ios/internals/EnrichedTextInputViewState.cpp +10 -0
  101. package/ios/internals/EnrichedTextInputViewState.h +20 -0
  102. package/ios/styles/BlockQuoteStyle.mm +248 -0
  103. package/ios/styles/BoldStyle.mm +122 -0
  104. package/ios/styles/H1Style.mm +10 -0
  105. package/ios/styles/H2Style.mm +10 -0
  106. package/ios/styles/H3Style.mm +10 -0
  107. package/ios/styles/HeadingStyleBase.mm +144 -0
  108. package/ios/styles/InlineCodeStyle.mm +163 -0
  109. package/ios/styles/ItalicStyle.mm +110 -0
  110. package/ios/styles/LinkStyle.mm +463 -0
  111. package/ios/styles/MentionStyle.mm +476 -0
  112. package/ios/styles/OrderedListStyle.mm +225 -0
  113. package/ios/styles/StrikethroughStyle.mm +80 -0
  114. package/ios/styles/UnderlineStyle.mm +112 -0
  115. package/ios/styles/UnorderedListStyle.mm +225 -0
  116. package/ios/utils/BaseStyleProtocol.h +16 -0
  117. package/ios/utils/ColorExtension.h +6 -0
  118. package/ios/utils/ColorExtension.mm +27 -0
  119. package/ios/utils/FontExtension.h +13 -0
  120. package/ios/utils/FontExtension.mm +91 -0
  121. package/ios/utils/LayoutManagerExtension.h +6 -0
  122. package/ios/utils/LayoutManagerExtension.mm +171 -0
  123. package/ios/utils/LinkData.h +9 -0
  124. package/ios/utils/LinkData.mm +4 -0
  125. package/ios/utils/MentionParams.h +9 -0
  126. package/ios/utils/MentionParams.mm +4 -0
  127. package/ios/utils/MentionStyleProps.h +13 -0
  128. package/ios/utils/MentionStyleProps.mm +56 -0
  129. package/ios/utils/OccurenceUtils.h +37 -0
  130. package/ios/utils/OccurenceUtils.mm +124 -0
  131. package/ios/utils/ParagraphsUtils.h +7 -0
  132. package/ios/utils/ParagraphsUtils.mm +54 -0
  133. package/ios/utils/StringExtension.h +15 -0
  134. package/ios/utils/StringExtension.mm +57 -0
  135. package/ios/utils/StyleHeaders.h +74 -0
  136. package/ios/utils/StylePair.h +9 -0
  137. package/ios/utils/StylePair.mm +4 -0
  138. package/ios/utils/StyleTypeEnum.h +22 -0
  139. package/ios/utils/TextDecorationLineEnum.h +6 -0
  140. package/ios/utils/TextDecorationLineEnum.mm +4 -0
  141. package/ios/utils/TextInsertionUtils.h +6 -0
  142. package/ios/utils/TextInsertionUtils.mm +48 -0
  143. package/ios/utils/WordsUtils.h +6 -0
  144. package/ios/utils/WordsUtils.mm +88 -0
  145. package/ios/utils/ZeroWidthSpaceUtils.h +7 -0
  146. package/ios/utils/ZeroWidthSpaceUtils.mm +164 -0
  147. package/lib/module/EnrichedTextInput.js +191 -0
  148. package/lib/module/EnrichedTextInput.js.map +1 -0
  149. package/lib/module/EnrichedTextInputNativeComponent.ts +235 -0
  150. package/lib/module/index.js +4 -0
  151. package/lib/module/index.js.map +1 -0
  152. package/lib/module/normalizeHtmlStyle.js +141 -0
  153. package/lib/module/normalizeHtmlStyle.js.map +1 -0
  154. package/lib/module/package.json +1 -0
  155. package/lib/typescript/package.json +1 -0
  156. package/lib/typescript/src/EnrichedTextInput.d.ts +113 -0
  157. package/lib/typescript/src/EnrichedTextInput.d.ts.map +1 -0
  158. package/lib/typescript/src/EnrichedTextInputNativeComponent.d.ts +160 -0
  159. package/lib/typescript/src/EnrichedTextInputNativeComponent.d.ts.map +1 -0
  160. package/lib/typescript/src/index.d.ts +3 -0
  161. package/lib/typescript/src/index.d.ts.map +1 -0
  162. package/lib/typescript/src/normalizeHtmlStyle.d.ts +4 -0
  163. package/lib/typescript/src/normalizeHtmlStyle.d.ts.map +1 -0
  164. package/package.json +172 -1
  165. package/react-native.config.js +13 -0
  166. package/src/EnrichedTextInput.tsx +358 -0
  167. package/src/EnrichedTextInputNativeComponent.ts +235 -0
  168. package/src/index.tsx +9 -0
  169. package/src/normalizeHtmlStyle.ts +188 -0
@@ -0,0 +1,17 @@
1
+
2
+ /**
3
+ * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
4
+ *
5
+ * Do not edit this file as changes may cause incorrect behavior and will be lost
6
+ * once the code is regenerated.
7
+ *
8
+ * @generated by codegen project: GenerateShadowNodeCpp.js
9
+ */
10
+
11
+ #include "ShadowNodes.h"
12
+
13
+ namespace facebook::react {
14
+
15
+
16
+
17
+ } // namespace facebook::react
@@ -0,0 +1,23 @@
1
+
2
+ /**
3
+ * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
4
+ *
5
+ * Do not edit this file as changes may cause incorrect behavior and will be lost
6
+ * once the code is regenerated.
7
+ *
8
+ * @generated by codegen project: GenerateShadowNodeH.js
9
+ */
10
+
11
+ #pragma once
12
+
13
+ #include "EventEmitters.h"
14
+ #include "Props.h"
15
+ #include "States.h"
16
+ #include <react/renderer/components/view/ConcreteViewShadowNode.h>
17
+ #include <jsi/jsi.h>
18
+
19
+ namespace facebook::react {
20
+
21
+
22
+
23
+ } // namespace facebook::react
@@ -0,0 +1,16 @@
1
+
2
+ /**
3
+ * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
4
+ *
5
+ * Do not edit this file as changes may cause incorrect behavior and will be lost
6
+ * once the code is regenerated.
7
+ *
8
+ * @generated by codegen project: GenerateStateCpp.js
9
+ */
10
+ #include "States.h"
11
+
12
+ namespace facebook::react {
13
+
14
+
15
+
16
+ } // namespace facebook::react
@@ -0,0 +1,20 @@
1
+ /**
2
+ * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
3
+ *
4
+ * Do not edit this file as changes may cause incorrect behavior and will be lost
5
+ * once the code is regenerated.
6
+ *
7
+ * @generated by codegen project: GenerateStateH.js
8
+ */
9
+ #pragma once
10
+
11
+ #include <react/renderer/core/StateData.h>
12
+ #ifdef RN_SERIALIZABLE_STATE
13
+ #include <folly/dynamic.h>
14
+ #endif
15
+
16
+ namespace facebook::react {
17
+
18
+
19
+
20
+ } // namespace facebook::react
@@ -0,0 +1,5 @@
1
+ EnrichedTextInput_kotlinVersion=2.0.21
2
+ EnrichedTextInput_minSdkVersion=24
3
+ EnrichedTextInput_targetSdkVersion=34
4
+ EnrichedTextInput_compileSdkVersion=35
5
+ EnrichedTextInput__ndkVersion=27.1.12297006
@@ -0,0 +1,3 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
2
+ package="com.swmansion.enriched">
3
+ </manifest>
@@ -0,0 +1,2 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ </manifest>
@@ -0,0 +1,535 @@
1
+ package com.swmansion.enriched
2
+
3
+ import android.content.ClipData
4
+ import android.content.ClipboardManager
5
+ import android.content.Context
6
+ import android.graphics.BlendMode
7
+ import android.graphics.BlendModeColorFilter
8
+ import android.graphics.Color
9
+ import android.graphics.Rect
10
+ import android.os.Build
11
+ import android.text.InputType
12
+ import android.text.Spannable
13
+ import android.util.AttributeSet
14
+ import android.util.Log
15
+ import android.util.TypedValue
16
+ import android.view.Gravity
17
+ import android.view.MotionEvent
18
+ import android.view.inputmethod.InputMethodManager
19
+ import androidx.appcompat.widget.AppCompatEditText
20
+ import com.facebook.react.bridge.ReactContext
21
+ import com.facebook.react.common.ReactConstants
22
+ import com.facebook.react.uimanager.PixelUtil
23
+ import com.facebook.react.uimanager.StateWrapper
24
+ import com.facebook.react.uimanager.UIManagerHelper
25
+ import com.facebook.react.views.text.ReactTypefaceUtils.applyStyles
26
+ import com.facebook.react.views.text.ReactTypefaceUtils.parseFontStyle
27
+ import com.facebook.react.views.text.ReactTypefaceUtils.parseFontWeight
28
+ import com.swmansion.enriched.events.MentionHandler
29
+ import com.swmansion.enriched.events.OnInputBlurEvent
30
+ import com.swmansion.enriched.events.OnInputFocusEvent
31
+ import com.swmansion.enriched.spans.EnrichedSpans
32
+ import com.swmansion.enriched.styles.InlineStyles
33
+ import com.swmansion.enriched.styles.ListStyles
34
+ import com.swmansion.enriched.styles.ParagraphStyles
35
+ import com.swmansion.enriched.styles.ParametrizedStyles
36
+ import com.swmansion.enriched.styles.HtmlStyle
37
+ import com.swmansion.enriched.utils.EnrichedParser
38
+ import com.swmansion.enriched.utils.EnrichedSelection
39
+ import com.swmansion.enriched.utils.EnrichedSpanState
40
+ import com.swmansion.enriched.utils.mergeSpannables
41
+ import com.swmansion.enriched.watchers.EnrichedSpanWatcher
42
+ import com.swmansion.enriched.watchers.EnrichedTextWatcher
43
+ import kotlin.math.ceil
44
+
45
+
46
+ class EnrichedTextInputView : AppCompatEditText {
47
+ var stateWrapper: StateWrapper? = null
48
+ val selection: EnrichedSelection? = EnrichedSelection(this)
49
+ val spanState: EnrichedSpanState? = EnrichedSpanState(this)
50
+ val inlineStyles: InlineStyles? = InlineStyles(this)
51
+ val paragraphStyles: ParagraphStyles? = ParagraphStyles(this)
52
+ val listStyles: ListStyles? = ListStyles(this)
53
+ val parametrizedStyles: ParametrizedStyles? = ParametrizedStyles(this)
54
+ var isSettingValue: Boolean = false
55
+ var isRemovingMany: Boolean = false
56
+
57
+ val mentionHandler: MentionHandler? = MentionHandler(this)
58
+ var htmlStyle: HtmlStyle = HtmlStyle(this, null)
59
+ var spanWatcher: EnrichedSpanWatcher? = null
60
+ var layoutManager: EnrichedTextInputViewLayoutManager = EnrichedTextInputViewLayoutManager(this)
61
+
62
+ var experimentalSynchronousEvents: Boolean = false
63
+
64
+ var fontSize: Float? = null
65
+ private var autoFocus = false
66
+ private var typefaceDirty = false
67
+ private var didAttachToWindow = false
68
+ private var detectScrollMovement = false
69
+ private var fontFamily: String? = null
70
+ private var fontStyle: Int = ReactConstants.UNSET
71
+ private var fontWeight: Int = ReactConstants.UNSET
72
+
73
+ private var inputMethodManager: InputMethodManager? = null
74
+
75
+ constructor(context: Context) : super(context) {
76
+ prepareComponent()
77
+ }
78
+
79
+ constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
80
+ prepareComponent()
81
+ }
82
+
83
+ constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(
84
+ context,
85
+ attrs,
86
+ defStyleAttr
87
+ ) {
88
+ prepareComponent()
89
+ }
90
+
91
+ init {
92
+ inputMethodManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
93
+ }
94
+
95
+ private fun prepareComponent() {
96
+ isSingleLine = false
97
+ isHorizontalScrollBarEnabled = false
98
+ isVerticalScrollBarEnabled = true
99
+ gravity = Gravity.TOP or Gravity.START
100
+ inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_MULTI_LINE
101
+
102
+ setPadding(0, 0, 0, 0)
103
+ setBackgroundColor(Color.TRANSPARENT)
104
+
105
+ addSpanWatcher(EnrichedSpanWatcher(this))
106
+ addTextChangedListener(EnrichedTextWatcher(this))
107
+ }
108
+
109
+ // https://github.com/facebook/react-native/blob/36df97f500aa0aa8031098caf7526db358b6ddc1/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt#L295C1-L296C1
110
+ override fun onTouchEvent(ev: MotionEvent): Boolean {
111
+ when (ev.action) {
112
+ MotionEvent.ACTION_DOWN -> {
113
+ detectScrollMovement = true
114
+ // Disallow parent views to intercept touch events, until we can detect if we should be
115
+ // capturing these touches or not.
116
+ this.parent.requestDisallowInterceptTouchEvent(true)
117
+ }
118
+
119
+ MotionEvent.ACTION_MOVE ->
120
+ if (detectScrollMovement) {
121
+ if (!canScrollVertically(-1) &&
122
+ !canScrollVertically(1) &&
123
+ !canScrollHorizontally(-1) &&
124
+ !canScrollHorizontally(1)) {
125
+ // We cannot scroll, let parent views take care of these touches.
126
+ this.parent.requestDisallowInterceptTouchEvent(false)
127
+ }
128
+ detectScrollMovement = false
129
+ }
130
+ }
131
+
132
+ return super.onTouchEvent(ev)
133
+ }
134
+
135
+ override fun onSelectionChanged(selStart: Int, selEnd: Int) {
136
+ super.onSelectionChanged(selStart, selEnd)
137
+ selection?.onSelection(selStart, selEnd)
138
+ }
139
+
140
+ override fun clearFocus() {
141
+ super.clearFocus()
142
+ inputMethodManager?.hideSoftInputFromWindow(windowToken, 0)
143
+ }
144
+
145
+ override fun onFocusChanged(focused: Boolean, direction: Int, previouslyFocusedRect: Rect?) {
146
+ super.onFocusChanged(focused, direction, previouslyFocusedRect)
147
+ val context = context as ReactContext
148
+ val surfaceId = UIManagerHelper.getSurfaceId(context)
149
+ val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(context, id)
150
+
151
+ if (focused) {
152
+ dispatcher?.dispatchEvent(OnInputFocusEvent(surfaceId, id, experimentalSynchronousEvents))
153
+ } else {
154
+ dispatcher?.dispatchEvent(OnInputBlurEvent(surfaceId, id, experimentalSynchronousEvents))
155
+ }
156
+ }
157
+
158
+ override fun onTextContextMenuItem(id: Int): Boolean {
159
+ when (id) {
160
+ android.R.id.copy -> {
161
+ handleCustomCopy()
162
+ return true
163
+ }
164
+ android.R.id.paste -> {
165
+ handleCustomPaste()
166
+ return true
167
+ }
168
+ }
169
+ return super.onTextContextMenuItem(id)
170
+ }
171
+
172
+ private fun handleCustomCopy() {
173
+ val start = selectionStart
174
+ val end = selectionEnd
175
+ val spannable = text as Spannable
176
+
177
+ if (start < end) {
178
+ val selectedText = spannable.subSequence(start, end) as Spannable
179
+ val selectedHtml = EnrichedParser.toHtml(selectedText)
180
+
181
+ val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
182
+ val clip = ClipData.newHtmlText(CLIPBOARD_TAG, selectedText, selectedHtml)
183
+ clipboard.setPrimaryClip(clip)
184
+ }
185
+ }
186
+
187
+ private fun handleCustomPaste() {
188
+ val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
189
+ if (!clipboard.hasPrimaryClip()) return
190
+
191
+ val clip = clipboard.primaryClip
192
+ val item = clip?.getItemAt(0)
193
+ val htmlText = item?.htmlText
194
+ val currentText = text as Spannable
195
+ val start = selection?.start ?: 0
196
+ val end = selection?.end ?: 0
197
+
198
+ if (htmlText != null) {
199
+ val parsedText = parseText(htmlText)
200
+ if (parsedText is Spannable) {
201
+ val finalText = currentText.mergeSpannables(start, end, parsedText)
202
+ setValue(finalText)
203
+ return
204
+ }
205
+ }
206
+
207
+ val finalText = currentText.mergeSpannables(start, end, item?.text.toString())
208
+ setValue(finalText)
209
+ parametrizedStyles?.detectAllLinks()
210
+ }
211
+
212
+ fun requestFocusProgrammatically() {
213
+ requestFocus()
214
+ inputMethodManager?.showSoftInput(this, 0)
215
+ setSelection(selection?.start ?: text?.length ?: 0)
216
+ }
217
+
218
+ private fun parseText(text: CharSequence): CharSequence {
219
+ val isHtml = text.startsWith("<html>") && text.endsWith("</html>")
220
+ if (!isHtml) return text
221
+
222
+ try {
223
+ val parsed = EnrichedParser.fromHtml(text.toString(), htmlStyle, null)
224
+ val withoutLastNewLine = parsed.trimEnd('\n')
225
+ return withoutLastNewLine
226
+ } catch (e: Exception) {
227
+ Log.e("EnrichedTextInputView", "Error parsing HTML: ${e.message}")
228
+ return text
229
+ }
230
+ }
231
+
232
+ fun setValue(value: CharSequence?) {
233
+ if (value == null) return
234
+ isSettingValue = true
235
+
236
+ val newText = parseText(value)
237
+ setText(newText)
238
+
239
+ // Assign SpanWatcher one more time as our previous spannable has been replaced
240
+ addSpanWatcher(EnrichedSpanWatcher(this))
241
+
242
+ // Scroll to the last line of text
243
+ setSelection(text?.length ?: 0)
244
+
245
+ isSettingValue = false
246
+ }
247
+
248
+ fun setAutoFocus(autoFocus: Boolean) {
249
+ this.autoFocus = autoFocus
250
+ }
251
+
252
+ fun setPlaceholder(placeholder: String?) {
253
+ if (placeholder == null) return
254
+
255
+ hint = placeholder
256
+ }
257
+
258
+ fun setPlaceholderTextColor(colorInt: Int?) {
259
+ if (colorInt == null) return
260
+
261
+ setHintTextColor(colorInt)
262
+ }
263
+
264
+ fun setSelectionColor(colorInt: Int?) {
265
+ if (colorInt == null) return
266
+
267
+ highlightColor = colorInt
268
+ }
269
+
270
+ fun setCursorColor(colorInt: Int?) {
271
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
272
+ val cursorDrawable = textCursorDrawable
273
+ if (cursorDrawable == null) return
274
+
275
+ if (colorInt != null) {
276
+ cursorDrawable.colorFilter = BlendModeColorFilter(colorInt, BlendMode.SRC_IN)
277
+ } else {
278
+ cursorDrawable.clearColorFilter()
279
+ }
280
+
281
+ textCursorDrawable = cursorDrawable
282
+ }
283
+ }
284
+
285
+ fun setColor(colorInt: Int?) {
286
+ if (colorInt == null) {
287
+ setTextColor(Color.BLACK)
288
+ return
289
+ }
290
+
291
+ setTextColor(colorInt)
292
+ }
293
+
294
+ fun setFontSize(size: Float) {
295
+ if (size == 0f) return
296
+
297
+ val sizeInt = ceil(PixelUtil.toPixelFromSP(size))
298
+ fontSize = sizeInt
299
+ setTextSize(TypedValue.COMPLEX_UNIT_PX, sizeInt)
300
+
301
+ // This ensured that newly created spans will take the new font size into account
302
+ htmlStyle.invalidateStyles()
303
+ layoutManager.invalidateLayout(text)
304
+ }
305
+
306
+ fun setFontFamily(family: String?) {
307
+ if (family != fontFamily) {
308
+ fontFamily = family
309
+ typefaceDirty = true
310
+ }
311
+ }
312
+
313
+ fun setFontWeight(weight: String?) {
314
+ val fontWeight = parseFontWeight(weight)
315
+
316
+ if (fontWeight != fontStyle) {
317
+ this.fontWeight = fontWeight
318
+ typefaceDirty = true
319
+ }
320
+ }
321
+
322
+ fun setFontStyle(style: String?) {
323
+ val fontStyle = parseFontStyle(style)
324
+
325
+ if (fontStyle != this.fontStyle) {
326
+ this.fontStyle = fontStyle
327
+ typefaceDirty = true
328
+ }
329
+ }
330
+
331
+ fun setAutoCapitalize(flagName: String?) {
332
+ val flag = when (flagName) {
333
+ "none" -> InputType.TYPE_NULL
334
+ "sentences" -> InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
335
+ "words" -> InputType.TYPE_TEXT_FLAG_CAP_WORDS
336
+ "characters" -> InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS
337
+ else -> InputType.TYPE_NULL
338
+ }
339
+
340
+ inputType = (inputType and
341
+ InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS.inv() and
342
+ InputType.TYPE_TEXT_FLAG_CAP_WORDS.inv() and
343
+ InputType.TYPE_TEXT_FLAG_CAP_SENTENCES.inv()
344
+ ) or if (flag == InputType.TYPE_NULL) 0 else flag
345
+ }
346
+
347
+ // https://github.com/facebook/react-native/blob/36df97f500aa0aa8031098caf7526db358b6ddc1/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt#L283C2-L284C1
348
+ // After the text changes inside an EditText, TextView checks if a layout() has been requested.
349
+ // If it has, it will not scroll the text to the end of the new text inserted, but wait for the
350
+ // next layout() to be called. However, we do not perform a layout() after a requestLayout(), so
351
+ // we need to override isLayoutRequested to force EditText to scroll to the end of the new text
352
+ // immediately.
353
+ override fun isLayoutRequested(): Boolean {
354
+ return false
355
+ }
356
+
357
+ fun updateTypeface() {
358
+ if (!typefaceDirty) return
359
+ typefaceDirty = false
360
+
361
+ val newTypeface = applyStyles(typeface, fontStyle, fontWeight, fontFamily, context.assets)
362
+ typeface = newTypeface
363
+ paint.typeface = newTypeface
364
+
365
+ layoutManager.invalidateLayout(text)
366
+ }
367
+
368
+ private fun toggleStyle(name: String) {
369
+ when (name) {
370
+ EnrichedSpans.BOLD -> inlineStyles?.toggleStyle(EnrichedSpans.BOLD)
371
+ EnrichedSpans.ITALIC -> inlineStyles?.toggleStyle(EnrichedSpans.ITALIC)
372
+ EnrichedSpans.UNDERLINE -> inlineStyles?.toggleStyle(EnrichedSpans.UNDERLINE)
373
+ EnrichedSpans.STRIKETHROUGH -> inlineStyles?.toggleStyle(EnrichedSpans.STRIKETHROUGH)
374
+ EnrichedSpans.INLINE_CODE -> inlineStyles?.toggleStyle(EnrichedSpans.INLINE_CODE)
375
+ EnrichedSpans.H1 -> paragraphStyles?.toggleStyle(EnrichedSpans.H1)
376
+ EnrichedSpans.H2 -> paragraphStyles?.toggleStyle(EnrichedSpans.H2)
377
+ EnrichedSpans.H3 -> paragraphStyles?.toggleStyle(EnrichedSpans.H3)
378
+ EnrichedSpans.CODE_BLOCK -> paragraphStyles?.toggleStyle(EnrichedSpans.CODE_BLOCK)
379
+ EnrichedSpans.BLOCK_QUOTE -> paragraphStyles?.toggleStyle(EnrichedSpans.BLOCK_QUOTE)
380
+ EnrichedSpans.ORDERED_LIST -> listStyles?.toggleStyle(EnrichedSpans.ORDERED_LIST)
381
+ EnrichedSpans.UNORDERED_LIST -> listStyles?.toggleStyle(EnrichedSpans.UNORDERED_LIST)
382
+ else -> Log.w("EnrichedTextInputView", "Unknown style: $name")
383
+ }
384
+
385
+ layoutManager.invalidateLayout(text)
386
+ }
387
+
388
+ private fun removeStyle(name: String, start: Int, end: Int): Boolean {
389
+ val removed = when (name) {
390
+ EnrichedSpans.BOLD -> inlineStyles?.removeStyle(EnrichedSpans.BOLD, start, end)
391
+ EnrichedSpans.ITALIC -> inlineStyles?.removeStyle(EnrichedSpans.ITALIC, start, end)
392
+ EnrichedSpans.UNDERLINE -> inlineStyles?.removeStyle(EnrichedSpans.UNDERLINE, start, end)
393
+ EnrichedSpans.STRIKETHROUGH -> inlineStyles?.removeStyle(EnrichedSpans.STRIKETHROUGH, start, end)
394
+ EnrichedSpans.INLINE_CODE -> inlineStyles?.removeStyle(EnrichedSpans.INLINE_CODE, start, end)
395
+ EnrichedSpans.H1 -> paragraphStyles?.removeStyle(EnrichedSpans.H1, start, end)
396
+ EnrichedSpans.H2 -> paragraphStyles?.removeStyle(EnrichedSpans.H2, start, end)
397
+ EnrichedSpans.H3 -> paragraphStyles?.removeStyle(EnrichedSpans.H3, start, end)
398
+ EnrichedSpans.CODE_BLOCK -> paragraphStyles?.removeStyle(EnrichedSpans.CODE_BLOCK, start, end)
399
+ EnrichedSpans.BLOCK_QUOTE -> paragraphStyles?.removeStyle(EnrichedSpans.BLOCK_QUOTE, start, end)
400
+ EnrichedSpans.ORDERED_LIST -> listStyles?.removeStyle(EnrichedSpans.ORDERED_LIST, start, end)
401
+ EnrichedSpans.UNORDERED_LIST -> listStyles?.removeStyle(EnrichedSpans.UNORDERED_LIST, start, end)
402
+ EnrichedSpans.LINK -> parametrizedStyles?.removeStyle(EnrichedSpans.LINK, start, end)
403
+ EnrichedSpans.IMAGE -> parametrizedStyles?.removeStyle(EnrichedSpans.IMAGE, start, end)
404
+ EnrichedSpans.MENTION -> parametrizedStyles?.removeStyle(EnrichedSpans.MENTION, start, end)
405
+ else -> false
406
+ }
407
+
408
+ return removed == true
409
+ }
410
+
411
+ private fun getTargetRange(name: String): Pair<Int, Int> {
412
+ val result = when (name) {
413
+ EnrichedSpans.BOLD -> inlineStyles?.getStyleRange()
414
+ EnrichedSpans.ITALIC -> inlineStyles?.getStyleRange()
415
+ EnrichedSpans.UNDERLINE -> inlineStyles?.getStyleRange()
416
+ EnrichedSpans.STRIKETHROUGH -> inlineStyles?.getStyleRange()
417
+ EnrichedSpans.INLINE_CODE -> inlineStyles?.getStyleRange()
418
+ EnrichedSpans.H1 -> paragraphStyles?.getStyleRange()
419
+ EnrichedSpans.H2 -> paragraphStyles?.getStyleRange()
420
+ EnrichedSpans.H3 -> paragraphStyles?.getStyleRange()
421
+ EnrichedSpans.CODE_BLOCK -> paragraphStyles?.getStyleRange()
422
+ EnrichedSpans.BLOCK_QUOTE -> paragraphStyles?.getStyleRange()
423
+ EnrichedSpans.ORDERED_LIST -> listStyles?.getStyleRange()
424
+ EnrichedSpans.UNORDERED_LIST -> listStyles?.getStyleRange()
425
+ EnrichedSpans.LINK -> parametrizedStyles?.getStyleRange()
426
+ EnrichedSpans.IMAGE -> parametrizedStyles?.getStyleRange()
427
+ EnrichedSpans.MENTION -> parametrizedStyles?.getStyleRange()
428
+ else -> Pair(0, 0)
429
+ }
430
+
431
+ return result ?: Pair(0, 0)
432
+ }
433
+
434
+ private fun verifyStyle(name: String): Boolean {
435
+ val mergingConfig = EnrichedSpans.mergingConfig[name] ?: return true
436
+ val conflictingStyles = mergingConfig.conflictingStyles
437
+ val blockingStyles = mergingConfig.blockingStyles
438
+ val isEnabling = spanState?.getStart(name) == null
439
+ if (!isEnabling) return true
440
+
441
+ for (style in blockingStyles) {
442
+ if (spanState?.getStart(style) != null) {
443
+ spanState.setStart(name, null)
444
+ return false
445
+ }
446
+ }
447
+
448
+ for (style in conflictingStyles) {
449
+ val start = selection?.start ?: 0
450
+ val end = selection?.end ?: 0
451
+ val lengthBefore = text?.length ?: 0
452
+
453
+ isSettingValue = true
454
+ val targetRange = getTargetRange(name)
455
+ val removed = removeStyle(style, targetRange.first, targetRange.second)
456
+ if (removed) {
457
+ spanState?.setStart(style, null)
458
+ }
459
+ isSettingValue = false
460
+
461
+ val lengthAfter = text?.length ?: 0
462
+ val charactersRemoved = lengthBefore - lengthAfter
463
+ val finalEnd = if (charactersRemoved > 0) {
464
+ (end - charactersRemoved).coerceAtLeast(0)
465
+ } else {
466
+ end
467
+ }
468
+
469
+ val finalStart = start.coerceAtLeast(0).coerceAtMost(finalEnd)
470
+ selection?.onSelection(finalStart, finalEnd)
471
+ }
472
+
473
+ return true
474
+ }
475
+
476
+ private fun addSpanWatcher(watcher: EnrichedSpanWatcher) {
477
+ val spannable = text as Spannable
478
+ spannable.setSpan(watcher, 0, spannable.length, Spannable.SPAN_INCLUSIVE_INCLUSIVE)
479
+ spanWatcher = watcher
480
+ }
481
+
482
+ fun verifyAndToggleStyle(name: String) {
483
+ val isValid = verifyStyle(name)
484
+ if (!isValid) return
485
+
486
+ toggleStyle(name)
487
+ }
488
+
489
+ fun addLink(start: Int, end: Int, text: String, url: String) {
490
+ val isValid = verifyStyle(EnrichedSpans.LINK)
491
+ if (!isValid) return
492
+
493
+ parametrizedStyles?.setLinkSpan(start, end, text, url)
494
+ }
495
+
496
+ fun addImage(src: String) {
497
+ val isValid = verifyStyle(EnrichedSpans.IMAGE)
498
+ if (!isValid) return
499
+
500
+ parametrizedStyles?.setImageSpan(src)
501
+ }
502
+
503
+ fun startMention(indicator: String) {
504
+ val isValid = verifyStyle(EnrichedSpans.MENTION)
505
+ if (!isValid) return
506
+
507
+ parametrizedStyles?.startMention(indicator)
508
+ }
509
+
510
+ fun addMention(indicator: String, text: String, attributes: Map<String, String>) {
511
+ val isValid = verifyStyle(EnrichedSpans.MENTION)
512
+ if (!isValid) return
513
+
514
+ parametrizedStyles?.setMentionSpan(text, indicator, attributes)
515
+ }
516
+
517
+ override fun onAttachedToWindow() {
518
+ super.onAttachedToWindow()
519
+
520
+ if (autoFocus && !didAttachToWindow) {
521
+ requestFocusProgrammatically()
522
+ }
523
+
524
+ didAttachToWindow = true
525
+ }
526
+
527
+ override fun onDetachedFromWindow() {
528
+ layoutManager.cleanup()
529
+ super.onDetachedFromWindow()
530
+ }
531
+
532
+ companion object {
533
+ const val CLIPBOARD_TAG = "react-native-enriched-clipboard"
534
+ }
535
+ }
@@ -0,0 +1,64 @@
1
+ package com.swmansion.enriched
2
+
3
+ import android.text.Editable
4
+ import android.text.StaticLayout
5
+ import com.facebook.react.bridge.Arguments
6
+ import com.facebook.react.uimanager.PixelUtil
7
+
8
+ class EnrichedTextInputViewLayoutManager(private val view: EnrichedTextInputView) {
9
+ private var cachedSize: Pair<Float, Float> = Pair(0f, 0f)
10
+ private var cachedYogaWidth: Float = 0f
11
+ private var forceHeightRecalculationCounter: Int = 0
12
+
13
+ fun cleanup() {
14
+ forceHeightRecalculationCounter = 0
15
+ }
16
+
17
+ // Update shadow node's state in order to recalculate layout
18
+ fun invalidateLayout(text: Editable?) {
19
+ measureSize(text ?: "")
20
+
21
+ val counter = forceHeightRecalculationCounter
22
+ forceHeightRecalculationCounter++
23
+ val state = Arguments.createMap()
24
+ state.putInt("forceHeightRecalculationCounter", counter)
25
+ view.stateWrapper?.updateState(state)
26
+ }
27
+
28
+ fun getMeasuredSize(maxWidth: Float): Pair<Float, Float> {
29
+ if (maxWidth == cachedYogaWidth) {
30
+ return cachedSize
31
+ }
32
+
33
+ val text = view.text ?: ""
34
+ val result = measureAndCacheSize(text, maxWidth)
35
+ cachedYogaWidth = maxWidth
36
+ return result
37
+ }
38
+
39
+ fun measureSize(text: CharSequence): Pair<Float, Float> {
40
+ return measureAndCacheSize(text, cachedYogaWidth)
41
+ }
42
+
43
+ private fun measureAndCacheSize(text: CharSequence, maxWidth: Float): Pair<Float, Float> {
44
+ val result = measureSize(text, maxWidth)
45
+ cachedSize = result
46
+ return result
47
+ }
48
+
49
+ private fun measureSize(text: CharSequence, maxWidth: Float): Pair<Float, Float> {
50
+ val paint = view.paint
51
+ val textLength = text.length
52
+
53
+ val staticLayout = StaticLayout.Builder
54
+ .obtain(text, 0, textLength, paint, maxWidth.toInt())
55
+ .setIncludePad(true)
56
+ .setLineSpacing(0f, 1f)
57
+ .build()
58
+
59
+ val heightInSP = PixelUtil.toDIPFromPixel(staticLayout.height.toFloat())
60
+ val widthInSP = PixelUtil.toDIPFromPixel(maxWidth)
61
+
62
+ return Pair(widthInSP, heightInSP)
63
+ }
64
+ }