react-native-enriched 0.2.1 → 0.3.0

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 (156) hide show
  1. package/README.md +15 -12
  2. package/android/build.gradle +77 -72
  3. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedTextInputViewManagerDelegate.java +18 -0
  4. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedTextInputViewManagerInterface.java +6 -0
  5. package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/EventEmitters.cpp +146 -0
  6. package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/EventEmitters.h +140 -0
  7. package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/Props.cpp +10 -0
  8. package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/Props.h +194 -0
  9. package/android/lint.gradle +70 -0
  10. package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputConnectionWrapper.kt +140 -0
  11. package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputView.kt +245 -116
  12. package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewLayoutManager.kt +3 -1
  13. package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewManager.kt +162 -53
  14. package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewPackage.kt +1 -3
  15. package/android/src/main/java/com/swmansion/enriched/MeasurementStore.kt +70 -21
  16. package/android/src/main/java/com/swmansion/enriched/events/MentionHandler.kt +20 -10
  17. package/android/src/main/java/com/swmansion/enriched/events/OnChangeHtmlEvent.kt +8 -9
  18. package/android/src/main/java/com/swmansion/enriched/events/OnChangeSelectionEvent.kt +10 -9
  19. package/android/src/main/java/com/swmansion/enriched/events/OnChangeStateDeprecatedEvent.kt +21 -0
  20. package/android/src/main/java/com/swmansion/enriched/events/OnChangeStateEvent.kt +9 -12
  21. package/android/src/main/java/com/swmansion/enriched/events/OnChangeTextEvent.kt +10 -10
  22. package/android/src/main/java/com/swmansion/enriched/events/OnInputBlurEvent.kt +7 -9
  23. package/android/src/main/java/com/swmansion/enriched/events/OnInputFocusEvent.kt +7 -9
  24. package/android/src/main/java/com/swmansion/enriched/events/OnInputKeyPressEvent.kt +27 -0
  25. package/android/src/main/java/com/swmansion/enriched/events/OnLinkDetectedEvent.kt +13 -11
  26. package/android/src/main/java/com/swmansion/enriched/events/OnMentionDetectedEvent.kt +10 -9
  27. package/android/src/main/java/com/swmansion/enriched/events/OnMentionEvent.kt +9 -8
  28. package/android/src/main/java/com/swmansion/enriched/events/OnRequestHtmlResultEvent.kt +1 -2
  29. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedBlockQuoteSpan.kt +21 -8
  30. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedBoldSpan.kt +5 -4
  31. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedCodeBlockSpan.kt +7 -5
  32. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedH1Span.kt +5 -4
  33. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedH2Span.kt +5 -4
  34. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedH3Span.kt +5 -4
  35. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedH4Span.kt +24 -0
  36. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedH5Span.kt +24 -0
  37. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedH6Span.kt +24 -0
  38. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedImageSpan.kt +29 -17
  39. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedInlineCodeSpan.kt +5 -4
  40. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedItalicSpan.kt +5 -4
  41. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedLinkSpan.kt +7 -7
  42. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedMentionSpan.kt +11 -14
  43. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedOrderedListSpan.kt +15 -14
  44. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedSpans.kt +167 -71
  45. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedStrikeThroughSpan.kt +5 -4
  46. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedUnderlineSpan.kt +5 -4
  47. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedUnorderedListSpan.kt +8 -8
  48. package/android/src/main/java/com/swmansion/enriched/spans/interfaces/EnrichedBlockSpan.kt +3 -2
  49. package/android/src/main/java/com/swmansion/enriched/spans/interfaces/EnrichedHeadingSpan.kt +1 -2
  50. package/android/src/main/java/com/swmansion/enriched/spans/interfaces/EnrichedInlineSpan.kt +1 -2
  51. package/android/src/main/java/com/swmansion/enriched/spans/interfaces/EnrichedParagraphSpan.kt +3 -2
  52. package/android/src/main/java/com/swmansion/enriched/spans/interfaces/EnrichedSpan.kt +1 -0
  53. package/android/src/main/java/com/swmansion/enriched/spans/interfaces/EnrichedZeroWidthSpaceSpan.kt +1 -2
  54. package/android/src/main/java/com/swmansion/enriched/spans/utils/ForceRedrawSpan.kt +2 -1
  55. package/android/src/main/java/com/swmansion/enriched/styles/HtmlStyle.kt +78 -21
  56. package/android/src/main/java/com/swmansion/enriched/styles/InlineStyles.kt +25 -8
  57. package/android/src/main/java/com/swmansion/enriched/styles/ListStyles.kt +60 -20
  58. package/android/src/main/java/com/swmansion/enriched/styles/ParagraphStyles.kt +86 -26
  59. package/android/src/main/java/com/swmansion/enriched/styles/ParametrizedStyles.kt +128 -52
  60. package/android/src/main/java/com/swmansion/enriched/utils/AsyncDrawable.kt +10 -7
  61. package/android/src/main/java/com/swmansion/enriched/utils/EnrichedConstants.kt +11 -0
  62. package/android/src/main/java/com/swmansion/enriched/utils/EnrichedEditableFactory.kt +17 -0
  63. package/android/src/main/java/com/swmansion/enriched/utils/EnrichedParser.java +128 -87
  64. package/android/src/main/java/com/swmansion/enriched/utils/EnrichedSelection.kt +71 -42
  65. package/android/src/main/java/com/swmansion/enriched/utils/EnrichedSpanState.kt +183 -48
  66. package/android/src/main/java/com/swmansion/enriched/utils/EnrichedSpannable.kt +82 -0
  67. package/android/src/main/java/com/swmansion/enriched/utils/EnrichedSpannableStringBuilder.kt +15 -0
  68. package/android/src/main/java/com/swmansion/enriched/utils/Utils.kt +0 -70
  69. package/android/src/main/java/com/swmansion/enriched/watchers/EnrichedSpanWatcher.kt +46 -14
  70. package/android/src/main/java/com/swmansion/enriched/watchers/EnrichedTextWatcher.kt +34 -11
  71. package/android/src/main/new_arch/CMakeLists.txt +6 -0
  72. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/conversions.h +21 -1
  73. package/ios/EnrichedTextInputView.h +1 -1
  74. package/ios/EnrichedTextInputView.mm +381 -49
  75. package/ios/config/InputConfig.h +18 -0
  76. package/ios/config/InputConfig.mm +118 -8
  77. package/ios/generated/RNEnrichedTextInputViewSpec/EventEmitters.cpp +146 -0
  78. package/ios/generated/RNEnrichedTextInputViewSpec/EventEmitters.h +140 -0
  79. package/ios/generated/RNEnrichedTextInputViewSpec/Props.cpp +10 -0
  80. package/ios/generated/RNEnrichedTextInputViewSpec/Props.h +194 -0
  81. package/ios/generated/RNEnrichedTextInputViewSpec/RCTComponentViewHelpers.h +74 -0
  82. package/ios/inputParser/InputParser.mm +83 -10
  83. package/ios/{attachments → interfaces}/ImageAttachment.mm +3 -1
  84. package/ios/interfaces/LinkRegexConfig.h +19 -0
  85. package/ios/interfaces/LinkRegexConfig.mm +37 -0
  86. package/ios/{utils → interfaces}/MentionStyleProps.mm +2 -2
  87. package/ios/{utils → interfaces}/StyleHeaders.h +10 -0
  88. package/ios/{utils → interfaces}/StyleTypeEnum.h +3 -0
  89. package/ios/styles/BlockQuoteStyle.mm +5 -5
  90. package/ios/styles/BoldStyle.mm +21 -6
  91. package/ios/styles/CodeBlockStyle.mm +5 -5
  92. package/ios/styles/H4Style.mm +17 -0
  93. package/ios/styles/H5Style.mm +17 -0
  94. package/ios/styles/H6Style.mm +17 -0
  95. package/ios/styles/HeadingStyleBase.mm +27 -10
  96. package/ios/styles/ImageStyle.mm +5 -5
  97. package/ios/styles/InlineCodeStyle.mm +30 -19
  98. package/ios/styles/ItalicStyle.mm +5 -5
  99. package/ios/styles/LinkStyle.mm +98 -40
  100. package/ios/styles/MentionStyle.mm +4 -4
  101. package/ios/styles/OrderedListStyle.mm +5 -5
  102. package/ios/styles/StrikethroughStyle.mm +5 -5
  103. package/ios/styles/UnderlineStyle.mm +5 -5
  104. package/ios/styles/UnorderedListStyle.mm +5 -5
  105. package/ios/utils/ParagraphAttributesUtils.h +4 -0
  106. package/ios/utils/ParagraphAttributesUtils.mm +67 -0
  107. package/ios/utils/ParagraphsUtils.mm +4 -4
  108. package/lib/module/EnrichedTextInput.js +22 -1
  109. package/lib/module/EnrichedTextInput.js.map +1 -1
  110. package/lib/module/EnrichedTextInputNativeComponent.ts +138 -12
  111. package/lib/module/{normalizeHtmlStyle.js → utils/normalizeHtmlStyle.js} +12 -0
  112. package/lib/module/utils/normalizeHtmlStyle.js.map +1 -0
  113. package/lib/module/utils/regexParser.js +46 -0
  114. package/lib/module/utils/regexParser.js.map +1 -0
  115. package/lib/typescript/src/EnrichedTextInput.d.ts +23 -14
  116. package/lib/typescript/src/EnrichedTextInput.d.ts.map +1 -1
  117. package/lib/typescript/src/EnrichedTextInputNativeComponent.d.ts +123 -12
  118. package/lib/typescript/src/EnrichedTextInputNativeComponent.d.ts.map +1 -1
  119. package/lib/typescript/src/index.d.ts +1 -1
  120. package/lib/typescript/src/index.d.ts.map +1 -1
  121. package/lib/typescript/src/utils/normalizeHtmlStyle.d.ts +4 -0
  122. package/lib/typescript/src/utils/normalizeHtmlStyle.d.ts.map +1 -0
  123. package/lib/typescript/src/utils/regexParser.d.ts +3 -0
  124. package/lib/typescript/src/utils/regexParser.d.ts.map +1 -0
  125. package/package.json +10 -6
  126. package/src/EnrichedTextInput.tsx +51 -13
  127. package/src/EnrichedTextInputNativeComponent.ts +138 -12
  128. package/src/index.tsx +2 -0
  129. package/src/{normalizeHtmlStyle.ts → utils/normalizeHtmlStyle.ts} +14 -2
  130. package/src/utils/regexParser.ts +56 -0
  131. package/lib/module/normalizeHtmlStyle.js.map +0 -1
  132. package/lib/typescript/src/normalizeHtmlStyle.d.ts +0 -4
  133. package/lib/typescript/src/normalizeHtmlStyle.d.ts.map +0 -1
  134. /package/ios/{utils → extensions}/ColorExtension.h +0 -0
  135. /package/ios/{utils → extensions}/ColorExtension.mm +0 -0
  136. /package/ios/{utils → extensions}/FontExtension.h +0 -0
  137. /package/ios/{utils → extensions}/FontExtension.mm +0 -0
  138. /package/ios/{utils → extensions}/LayoutManagerExtension.h +0 -0
  139. /package/ios/{utils → extensions}/LayoutManagerExtension.mm +0 -0
  140. /package/ios/{utils → extensions}/StringExtension.h +0 -0
  141. /package/ios/{utils → extensions}/StringExtension.mm +0 -0
  142. /package/ios/{utils → interfaces}/BaseStyleProtocol.h +0 -0
  143. /package/ios/{attachments → interfaces}/ImageAttachment.h +0 -0
  144. /package/ios/{utils → interfaces}/ImageData.h +0 -0
  145. /package/ios/{utils → interfaces}/ImageData.mm +0 -0
  146. /package/ios/{utils → interfaces}/LinkData.h +0 -0
  147. /package/ios/{utils → interfaces}/LinkData.mm +0 -0
  148. /package/ios/{attachments → interfaces}/MediaAttachment.h +0 -0
  149. /package/ios/{attachments → interfaces}/MediaAttachment.mm +0 -0
  150. /package/ios/{utils → interfaces}/MentionParams.h +0 -0
  151. /package/ios/{utils → interfaces}/MentionParams.mm +0 -0
  152. /package/ios/{utils → interfaces}/MentionStyleProps.h +0 -0
  153. /package/ios/{utils → interfaces}/StylePair.h +0 -0
  154. /package/ios/{utils → interfaces}/StylePair.mm +0 -0
  155. /package/ios/{utils → interfaces}/TextDecorationLineEnum.h +0 -0
  156. /package/ios/{utils → interfaces}/TextDecorationLineEnum.mm +0 -0
@@ -13,12 +13,16 @@ import android.text.InputType
13
13
  import android.text.Spannable
14
14
  import android.util.AttributeSet
15
15
  import android.util.Log
16
+ import android.util.Patterns
16
17
  import android.util.TypedValue
17
18
  import android.view.Gravity
18
19
  import android.view.MotionEvent
20
+ import android.view.inputmethod.EditorInfo
21
+ import android.view.inputmethod.InputConnection
19
22
  import android.view.inputmethod.InputMethodManager
20
23
  import androidx.appcompat.widget.AppCompatEditText
21
24
  import com.facebook.react.bridge.ReactContext
25
+ import com.facebook.react.bridge.ReadableMap
22
26
  import com.facebook.react.common.ReactConstants
23
27
  import com.facebook.react.uimanager.PixelUtil
24
28
  import com.facebook.react.uimanager.StateWrapper
@@ -36,20 +40,23 @@ import com.swmansion.enriched.spans.EnrichedH3Span
36
40
  import com.swmansion.enriched.spans.EnrichedImageSpan
37
41
  import com.swmansion.enriched.spans.EnrichedSpans
38
42
  import com.swmansion.enriched.spans.interfaces.EnrichedSpan
43
+ import com.swmansion.enriched.styles.HtmlStyle
39
44
  import com.swmansion.enriched.styles.InlineStyles
40
45
  import com.swmansion.enriched.styles.ListStyles
41
46
  import com.swmansion.enriched.styles.ParagraphStyles
42
47
  import com.swmansion.enriched.styles.ParametrizedStyles
43
- import com.swmansion.enriched.styles.HtmlStyle
48
+ import com.swmansion.enriched.utils.EnrichedConstants
49
+ import com.swmansion.enriched.utils.EnrichedEditableFactory
44
50
  import com.swmansion.enriched.utils.EnrichedParser
45
51
  import com.swmansion.enriched.utils.EnrichedSelection
46
52
  import com.swmansion.enriched.utils.EnrichedSpanState
47
53
  import com.swmansion.enriched.utils.mergeSpannables
48
54
  import com.swmansion.enriched.watchers.EnrichedSpanWatcher
49
55
  import com.swmansion.enriched.watchers.EnrichedTextWatcher
56
+ import java.util.regex.Pattern
57
+ import java.util.regex.PatternSyntaxException
50
58
  import kotlin.math.ceil
51
59
 
52
-
53
60
  class EnrichedTextInputView : AppCompatEditText {
54
61
  var stateWrapper: StateWrapper? = null
55
62
  val selection: EnrichedSelection? = EnrichedSelection(this)
@@ -65,16 +72,19 @@ class EnrichedTextInputView : AppCompatEditText {
65
72
  val mentionHandler: MentionHandler? = MentionHandler(this)
66
73
  var htmlStyle: HtmlStyle = HtmlStyle(this, null)
67
74
  set(value) {
68
- if (field != value) {
69
- val prev = field
70
- field = value
71
- reApplyHtmlStyleForSpans(prev, value)
72
- }
75
+ if (field != value) {
76
+ val prev = field
77
+ field = value
78
+ reApplyHtmlStyleForSpans(prev, value)
79
+ }
73
80
  }
81
+
82
+ var linkRegex: Pattern? = Patterns.WEB_URL
74
83
  var spanWatcher: EnrichedSpanWatcher? = null
75
84
  var layoutManager: EnrichedTextInputViewLayoutManager = EnrichedTextInputViewLayoutManager(this)
76
85
 
77
- var shouldEmitHtml: Boolean = true
86
+ var shouldEmitHtml: Boolean = false
87
+ var shouldEmitOnChangeText: Boolean = false
78
88
  var experimentalSynchronousEvents: Boolean = false
79
89
 
80
90
  var fontSize: Float? = null
@@ -101,11 +111,26 @@ class EnrichedTextInputView : AppCompatEditText {
101
111
  constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(
102
112
  context,
103
113
  attrs,
104
- defStyleAttr
114
+ defStyleAttr,
105
115
  ) {
106
116
  prepareComponent()
107
117
  }
108
118
 
119
+ override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection? {
120
+ var inputConnection = super.onCreateInputConnection(outAttrs)
121
+ if (inputConnection != null) {
122
+ inputConnection =
123
+ EnrichedTextInputConnectionWrapper(
124
+ inputConnection,
125
+ context as ReactContext,
126
+ this,
127
+ experimentalSynchronousEvents,
128
+ )
129
+ }
130
+
131
+ return inputConnection
132
+ }
133
+
109
134
  init {
110
135
  inputMethodManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
111
136
  }
@@ -124,7 +149,11 @@ class EnrichedTextInputView : AppCompatEditText {
124
149
  setPadding(0, 0, 0, 0)
125
150
  setBackgroundColor(Color.TRANSPARENT)
126
151
 
127
- addSpanWatcher(EnrichedSpanWatcher(this))
152
+ // Ensure that every time new editable is created, it has EnrichedSpanWatcher attached
153
+ val spanWatcher = EnrichedSpanWatcher(this)
154
+ this.spanWatcher = spanWatcher
155
+ setEditableFactory(EnrichedEditableFactory(spanWatcher))
156
+
128
157
  addTextChangedListener(EnrichedTextWatcher(this))
129
158
  }
130
159
 
@@ -138,31 +167,32 @@ class EnrichedTextInputView : AppCompatEditText {
138
167
  this.parent.requestDisallowInterceptTouchEvent(true)
139
168
  }
140
169
 
141
- MotionEvent.ACTION_MOVE ->
170
+ MotionEvent.ACTION_MOVE -> {
142
171
  if (detectScrollMovement) {
143
172
  if (!canScrollVertically(-1) &&
144
173
  !canScrollVertically(1) &&
145
174
  !canScrollHorizontally(-1) &&
146
- !canScrollHorizontally(1)) {
175
+ !canScrollHorizontally(1)
176
+ ) {
147
177
  // We cannot scroll, let parent views take care of these touches.
148
178
  this.parent.requestDisallowInterceptTouchEvent(false)
149
179
  }
150
180
  detectScrollMovement = false
151
181
  }
182
+ }
152
183
  }
153
184
 
154
185
  return super.onTouchEvent(ev)
155
186
  }
156
187
 
157
- override fun canScrollVertically(direction: Int): Boolean {
158
- return scrollEnabled
159
- }
188
+ override fun canScrollVertically(direction: Int): Boolean = scrollEnabled
160
189
 
161
- override fun canScrollHorizontally(direction: Int): Boolean {
162
- return scrollEnabled
163
- }
190
+ override fun canScrollHorizontally(direction: Int): Boolean = scrollEnabled
164
191
 
165
- override fun onSelectionChanged(selStart: Int, selEnd: Int) {
192
+ override fun onSelectionChanged(
193
+ selStart: Int,
194
+ selEnd: Int,
195
+ ) {
166
196
  super.onSelectionChanged(selStart, selEnd)
167
197
  selection?.onSelection(selStart, selEnd)
168
198
  }
@@ -172,7 +202,11 @@ class EnrichedTextInputView : AppCompatEditText {
172
202
  inputMethodManager?.hideSoftInputFromWindow(windowToken, 0)
173
203
  }
174
204
 
175
- override fun onFocusChanged(focused: Boolean, direction: Int, previouslyFocusedRect: Rect?) {
205
+ override fun onFocusChanged(
206
+ focused: Boolean,
207
+ direction: Int,
208
+ previouslyFocusedRect: Rect?,
209
+ ) {
176
210
  super.onFocusChanged(focused, direction, previouslyFocusedRect)
177
211
  val context = context as ReactContext
178
212
  val surfaceId = UIManagerHelper.getSurfaceId(context)
@@ -191,6 +225,7 @@ class EnrichedTextInputView : AppCompatEditText {
191
225
  handleCustomCopy()
192
226
  return true
193
227
  }
228
+
194
229
  android.R.id.paste -> {
195
230
  handleCustomPaste()
196
231
  return true
@@ -236,9 +271,13 @@ class EnrichedTextInputView : AppCompatEditText {
236
271
 
237
272
  // Currently, we do not support pasting images
238
273
  if (item?.text == null) return
274
+ val lengthBefore = currentText.length
239
275
  val finalText = currentText.mergeSpannables(start, end, item.text.toString())
240
276
  setValue(finalText)
241
- parametrizedStyles?.detectAllLinks()
277
+
278
+ // Detect links in the newly pasted range
279
+ val finalEndIndex = start + finalText.length - lengthBefore
280
+ parametrizedStyles?.detectLinksInRange(finalText, start, finalEndIndex)
242
281
  }
243
282
 
244
283
  fun requestFocusProgrammatically() {
@@ -269,14 +308,43 @@ class EnrichedTextInputView : AppCompatEditText {
269
308
  setText(newText)
270
309
 
271
310
  observeAsyncImages()
272
- // Assign SpanWatcher one more time as our previous spannable has been replaced
273
- addSpanWatcher(EnrichedSpanWatcher(this))
274
311
 
275
312
  // Scroll to the last line of text
276
313
  setSelection(text?.length ?: 0)
277
314
  }
278
315
  }
279
316
 
317
+ fun setCustomSelection(
318
+ visibleStart: Int,
319
+ visibleEnd: Int,
320
+ ) {
321
+ val actualStart = getActualIndex(visibleStart)
322
+ val actualEnd = getActualIndex(visibleEnd)
323
+
324
+ setSelection(actualStart, actualEnd)
325
+ }
326
+
327
+ // Helper: Walks through the string skipping ZWSPs to find the Nth visible character
328
+ private fun getActualIndex(visibleIndex: Int): Int {
329
+ val currentText = text as Spannable
330
+ var currentVisibleCount = 0
331
+ var actualIndex = 0
332
+
333
+ while (actualIndex < currentText.length) {
334
+ if (currentVisibleCount == visibleIndex) {
335
+ return actualIndex
336
+ }
337
+
338
+ // If the current char is not a hidden space, it counts towards our visible index
339
+ if (currentText[actualIndex] != EnrichedConstants.ZWS) {
340
+ currentVisibleCount++
341
+ }
342
+ actualIndex++
343
+ }
344
+
345
+ return actualIndex
346
+ }
347
+
280
348
  /**
281
349
  * Finds all async images in the current text and sets up listeners
282
350
  * to redraw the text layout when they finish downloading.
@@ -376,19 +444,50 @@ class EnrichedTextInputView : AppCompatEditText {
376
444
  }
377
445
 
378
446
  fun setAutoCapitalize(flagName: String?) {
379
- val flag = when (flagName) {
380
- "none" -> InputType.TYPE_NULL
381
- "sentences" -> InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
382
- "words" -> InputType.TYPE_TEXT_FLAG_CAP_WORDS
383
- "characters" -> InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS
384
- else -> InputType.TYPE_NULL
447
+ val flag =
448
+ when (flagName) {
449
+ "none" -> InputType.TYPE_NULL
450
+ "sentences" -> InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
451
+ "words" -> InputType.TYPE_TEXT_FLAG_CAP_WORDS
452
+ "characters" -> InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS
453
+ else -> InputType.TYPE_NULL
454
+ }
455
+
456
+ inputType = (
457
+ inputType and
458
+ InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS.inv() and
459
+ InputType.TYPE_TEXT_FLAG_CAP_WORDS.inv() and
460
+ InputType.TYPE_TEXT_FLAG_CAP_SENTENCES.inv()
461
+ ) or if (flag == InputType.TYPE_NULL) 0 else flag
462
+ }
463
+
464
+ fun setLinkRegex(config: ReadableMap?) {
465
+ val patternStr = config?.getString("pattern")
466
+ if (patternStr == null) {
467
+ linkRegex = Patterns.WEB_URL
468
+ return
469
+ }
470
+
471
+ if (config.getBoolean("isDefault")) {
472
+ linkRegex = Patterns.WEB_URL
473
+ return
385
474
  }
386
475
 
387
- inputType = (inputType and
388
- InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS.inv() and
389
- InputType.TYPE_TEXT_FLAG_CAP_WORDS.inv() and
390
- InputType.TYPE_TEXT_FLAG_CAP_SENTENCES.inv()
391
- ) or if (flag == InputType.TYPE_NULL) 0 else flag
476
+ if (config.getBoolean("isDisabled")) {
477
+ linkRegex = null
478
+ return
479
+ }
480
+
481
+ var flags = 0
482
+ if (config.getBoolean("caseInsensitive")) flags = flags or Pattern.CASE_INSENSITIVE
483
+ if (config.getBoolean("dotAll")) flags = flags or Pattern.DOTALL
484
+
485
+ try {
486
+ linkRegex = Pattern.compile("(?s).*?($patternStr).*", flags)
487
+ } catch (e: PatternSyntaxException) {
488
+ Log.w("EnrichedTextInputView", "Invalid link regex pattern: $patternStr")
489
+ linkRegex = Patterns.WEB_URL
490
+ }
392
491
  }
393
492
 
394
493
  // https://github.com/facebook/react-native/blob/36df97f500aa0aa8031098caf7526db358b6ddc1/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt#L283C2-L284C1
@@ -397,9 +496,7 @@ class EnrichedTextInputView : AppCompatEditText {
397
496
  // next layout() to be called. However, we do not perform a layout() after a requestLayout(), so
398
497
  // we need to override isLayoutRequested to force EditText to scroll to the end of the new text
399
498
  // immediately.
400
- override fun isLayoutRequested(): Boolean {
401
- return false
402
- }
499
+ override fun isLayoutRequested(): Boolean = false
403
500
 
404
501
  fun afterUpdateTransaction() {
405
502
  updateTypeface()
@@ -439,6 +536,9 @@ class EnrichedTextInputView : AppCompatEditText {
439
536
  EnrichedSpans.H1 -> paragraphStyles?.toggleStyle(EnrichedSpans.H1)
440
537
  EnrichedSpans.H2 -> paragraphStyles?.toggleStyle(EnrichedSpans.H2)
441
538
  EnrichedSpans.H3 -> paragraphStyles?.toggleStyle(EnrichedSpans.H3)
539
+ EnrichedSpans.H4 -> paragraphStyles?.toggleStyle(EnrichedSpans.H4)
540
+ EnrichedSpans.H5 -> paragraphStyles?.toggleStyle(EnrichedSpans.H5)
541
+ EnrichedSpans.H6 -> paragraphStyles?.toggleStyle(EnrichedSpans.H6)
442
542
  EnrichedSpans.CODE_BLOCK -> paragraphStyles?.toggleStyle(EnrichedSpans.CODE_BLOCK)
443
543
  EnrichedSpans.BLOCK_QUOTE -> paragraphStyles?.toggleStyle(EnrichedSpans.BLOCK_QUOTE)
444
544
  EnrichedSpans.ORDERED_LIST -> listStyles?.toggleStyle(EnrichedSpans.ORDERED_LIST)
@@ -449,48 +549,60 @@ class EnrichedTextInputView : AppCompatEditText {
449
549
  layoutManager.invalidateLayout()
450
550
  }
451
551
 
452
- private fun removeStyle(name: String, start: Int, end: Int): Boolean {
453
- val removed = when (name) {
454
- EnrichedSpans.BOLD -> inlineStyles?.removeStyle(EnrichedSpans.BOLD, start, end)
455
- EnrichedSpans.ITALIC -> inlineStyles?.removeStyle(EnrichedSpans.ITALIC, start, end)
456
- EnrichedSpans.UNDERLINE -> inlineStyles?.removeStyle(EnrichedSpans.UNDERLINE, start, end)
457
- EnrichedSpans.STRIKETHROUGH -> inlineStyles?.removeStyle(EnrichedSpans.STRIKETHROUGH, start, end)
458
- EnrichedSpans.INLINE_CODE -> inlineStyles?.removeStyle(EnrichedSpans.INLINE_CODE, start, end)
459
- EnrichedSpans.H1 -> paragraphStyles?.removeStyle(EnrichedSpans.H1, start, end)
460
- EnrichedSpans.H2 -> paragraphStyles?.removeStyle(EnrichedSpans.H2, start, end)
461
- EnrichedSpans.H3 -> paragraphStyles?.removeStyle(EnrichedSpans.H3, start, end)
462
- EnrichedSpans.CODE_BLOCK -> paragraphStyles?.removeStyle(EnrichedSpans.CODE_BLOCK, start, end)
463
- EnrichedSpans.BLOCK_QUOTE -> paragraphStyles?.removeStyle(EnrichedSpans.BLOCK_QUOTE, start, end)
464
- EnrichedSpans.ORDERED_LIST -> listStyles?.removeStyle(EnrichedSpans.ORDERED_LIST, start, end)
465
- EnrichedSpans.UNORDERED_LIST -> listStyles?.removeStyle(EnrichedSpans.UNORDERED_LIST, start, end)
466
- EnrichedSpans.LINK -> parametrizedStyles?.removeStyle(EnrichedSpans.LINK, start, end)
467
- EnrichedSpans.IMAGE -> parametrizedStyles?.removeStyle(EnrichedSpans.IMAGE, start, end)
468
- EnrichedSpans.MENTION -> parametrizedStyles?.removeStyle(EnrichedSpans.MENTION, start, end)
469
- else -> false
470
- }
552
+ private fun removeStyle(
553
+ name: String,
554
+ start: Int,
555
+ end: Int,
556
+ ): Boolean {
557
+ val removed =
558
+ when (name) {
559
+ EnrichedSpans.BOLD -> inlineStyles?.removeStyle(EnrichedSpans.BOLD, start, end)
560
+ EnrichedSpans.ITALIC -> inlineStyles?.removeStyle(EnrichedSpans.ITALIC, start, end)
561
+ EnrichedSpans.UNDERLINE -> inlineStyles?.removeStyle(EnrichedSpans.UNDERLINE, start, end)
562
+ EnrichedSpans.STRIKETHROUGH -> inlineStyles?.removeStyle(EnrichedSpans.STRIKETHROUGH, start, end)
563
+ EnrichedSpans.INLINE_CODE -> inlineStyles?.removeStyle(EnrichedSpans.INLINE_CODE, start, end)
564
+ EnrichedSpans.H1 -> paragraphStyles?.removeStyle(EnrichedSpans.H1, start, end)
565
+ EnrichedSpans.H2 -> paragraphStyles?.removeStyle(EnrichedSpans.H2, start, end)
566
+ EnrichedSpans.H3 -> paragraphStyles?.removeStyle(EnrichedSpans.H3, start, end)
567
+ EnrichedSpans.H4 -> paragraphStyles?.removeStyle(EnrichedSpans.H4, start, end)
568
+ EnrichedSpans.H5 -> paragraphStyles?.removeStyle(EnrichedSpans.H5, start, end)
569
+ EnrichedSpans.H6 -> paragraphStyles?.removeStyle(EnrichedSpans.H6, start, end)
570
+ EnrichedSpans.CODE_BLOCK -> paragraphStyles?.removeStyle(EnrichedSpans.CODE_BLOCK, start, end)
571
+ EnrichedSpans.BLOCK_QUOTE -> paragraphStyles?.removeStyle(EnrichedSpans.BLOCK_QUOTE, start, end)
572
+ EnrichedSpans.ORDERED_LIST -> listStyles?.removeStyle(EnrichedSpans.ORDERED_LIST, start, end)
573
+ EnrichedSpans.UNORDERED_LIST -> listStyles?.removeStyle(EnrichedSpans.UNORDERED_LIST, start, end)
574
+ EnrichedSpans.LINK -> parametrizedStyles?.removeStyle(EnrichedSpans.LINK, start, end)
575
+ EnrichedSpans.IMAGE -> parametrizedStyles?.removeStyle(EnrichedSpans.IMAGE, start, end)
576
+ EnrichedSpans.MENTION -> parametrizedStyles?.removeStyle(EnrichedSpans.MENTION, start, end)
577
+ else -> false
578
+ }
471
579
 
472
580
  return removed == true
473
581
  }
474
582
 
475
583
  private fun getTargetRange(name: String): Pair<Int, Int> {
476
- val result = when (name) {
477
- EnrichedSpans.BOLD -> inlineStyles?.getStyleRange()
478
- EnrichedSpans.ITALIC -> inlineStyles?.getStyleRange()
479
- EnrichedSpans.UNDERLINE -> inlineStyles?.getStyleRange()
480
- EnrichedSpans.STRIKETHROUGH -> inlineStyles?.getStyleRange()
481
- EnrichedSpans.INLINE_CODE -> inlineStyles?.getStyleRange()
482
- EnrichedSpans.H1 -> paragraphStyles?.getStyleRange()
483
- EnrichedSpans.H2 -> paragraphStyles?.getStyleRange()
484
- EnrichedSpans.H3 -> paragraphStyles?.getStyleRange()
485
- EnrichedSpans.CODE_BLOCK -> paragraphStyles?.getStyleRange()
486
- EnrichedSpans.BLOCK_QUOTE -> paragraphStyles?.getStyleRange()
487
- EnrichedSpans.ORDERED_LIST -> listStyles?.getStyleRange()
488
- EnrichedSpans.UNORDERED_LIST -> listStyles?.getStyleRange()
489
- EnrichedSpans.LINK -> parametrizedStyles?.getStyleRange()
490
- EnrichedSpans.IMAGE -> parametrizedStyles?.getStyleRange()
491
- EnrichedSpans.MENTION -> parametrizedStyles?.getStyleRange()
492
- else -> Pair(0, 0)
493
- }
584
+ val result =
585
+ when (name) {
586
+ EnrichedSpans.BOLD -> inlineStyles?.getStyleRange()
587
+ EnrichedSpans.ITALIC -> inlineStyles?.getStyleRange()
588
+ EnrichedSpans.UNDERLINE -> inlineStyles?.getStyleRange()
589
+ EnrichedSpans.STRIKETHROUGH -> inlineStyles?.getStyleRange()
590
+ EnrichedSpans.INLINE_CODE -> inlineStyles?.getStyleRange()
591
+ EnrichedSpans.H1 -> paragraphStyles?.getStyleRange()
592
+ EnrichedSpans.H2 -> paragraphStyles?.getStyleRange()
593
+ EnrichedSpans.H3 -> paragraphStyles?.getStyleRange()
594
+ EnrichedSpans.H4 -> paragraphStyles?.getStyleRange()
595
+ EnrichedSpans.H5 -> paragraphStyles?.getStyleRange()
596
+ EnrichedSpans.H6 -> paragraphStyles?.getStyleRange()
597
+ EnrichedSpans.CODE_BLOCK -> paragraphStyles?.getStyleRange()
598
+ EnrichedSpans.BLOCK_QUOTE -> paragraphStyles?.getStyleRange()
599
+ EnrichedSpans.ORDERED_LIST -> listStyles?.getStyleRange()
600
+ EnrichedSpans.UNORDERED_LIST -> listStyles?.getStyleRange()
601
+ EnrichedSpans.LINK -> parametrizedStyles?.getStyleRange()
602
+ EnrichedSpans.IMAGE -> parametrizedStyles?.getStyleRange()
603
+ EnrichedSpans.MENTION -> parametrizedStyles?.getStyleRange()
604
+ else -> Pair(0, 0)
605
+ }
494
606
 
495
607
  return result ?: Pair(0, 0)
496
608
  }
@@ -524,11 +636,12 @@ class EnrichedTextInputView : AppCompatEditText {
524
636
 
525
637
  val lengthAfter = text?.length ?: 0
526
638
  val charactersRemoved = lengthBefore - lengthAfter
527
- val finalEnd = if (charactersRemoved > 0) {
528
- (end - charactersRemoved).coerceAtLeast(0)
529
- } else {
530
- end
531
- }
639
+ val finalEnd =
640
+ if (charactersRemoved > 0) {
641
+ (end - charactersRemoved).coerceAtLeast(0)
642
+ } else {
643
+ end
644
+ }
532
645
 
533
646
  val finalStart = start.coerceAtLeast(0).coerceAtMost(finalEnd)
534
647
  selection?.onSelection(finalStart, finalEnd)
@@ -537,12 +650,6 @@ class EnrichedTextInputView : AppCompatEditText {
537
650
  return true
538
651
  }
539
652
 
540
- private fun addSpanWatcher(watcher: EnrichedSpanWatcher) {
541
- val spannable = text as Spannable
542
- spannable.setSpan(watcher, 0, spannable.length, Spannable.SPAN_INCLUSIVE_INCLUSIVE)
543
- spanWatcher = watcher
544
- }
545
-
546
653
  fun verifyAndToggleStyle(name: String) {
547
654
  val isValid = verifyStyle(name)
548
655
  if (!isValid) return
@@ -550,14 +657,23 @@ class EnrichedTextInputView : AppCompatEditText {
550
657
  toggleStyle(name)
551
658
  }
552
659
 
553
- fun addLink(start: Int, end: Int, text: String, url: String) {
660
+ fun addLink(
661
+ start: Int,
662
+ end: Int,
663
+ text: String,
664
+ url: String,
665
+ ) {
554
666
  val isValid = verifyStyle(EnrichedSpans.LINK)
555
667
  if (!isValid) return
556
668
 
557
669
  parametrizedStyles?.setLinkSpan(start, end, text, url)
558
670
  }
559
671
 
560
- fun addImage(src: String, width: Float, height: Float) {
672
+ fun addImage(
673
+ src: String,
674
+ width: Float,
675
+ height: Float,
676
+ ) {
561
677
  val isValid = verifyStyle(EnrichedSpans.IMAGE)
562
678
  if (!isValid) return
563
679
 
@@ -572,7 +688,11 @@ class EnrichedTextInputView : AppCompatEditText {
572
688
  parametrizedStyles?.startMention(indicator)
573
689
  }
574
690
 
575
- fun addMention(indicator: String, text: String, attributes: Map<String, String>) {
691
+ fun addMention(
692
+ indicator: String,
693
+ text: String,
694
+ attributes: Map<String, String>,
695
+ ) {
576
696
  val isValid = verifyStyle(EnrichedSpans.MENTION)
577
697
  if (!isValid) return
578
698
 
@@ -580,11 +700,12 @@ class EnrichedTextInputView : AppCompatEditText {
580
700
  }
581
701
 
582
702
  fun requestHTML(requestId: Int) {
583
- val html = try {
584
- EnrichedParser.toHtmlWithDefault(text)
585
- } catch (e: Exception) {
586
- null
587
- }
703
+ val html =
704
+ try {
705
+ EnrichedParser.toHtmlWithDefault(text)
706
+ } catch (e: Exception) {
707
+ null
708
+ }
588
709
 
589
710
  val reactContext = context as ReactContext
590
711
  val surfaceId = UIManagerHelper.getSurfaceId(reactContext)
@@ -604,34 +725,37 @@ class EnrichedTextInputView : AppCompatEditText {
604
725
  }
605
726
  }
606
727
 
607
- private fun forceScrollToSelection() {
608
- val textLayout = layout ?: return
609
- val cursorOffset = selectionStart
610
- if (cursorOffset <= 0) return
728
+ private fun forceScrollToSelection() {
729
+ val textLayout = layout ?: return
730
+ val cursorOffset = selectionStart
731
+ if (cursorOffset <= 0) return
611
732
 
612
- val selectedLineIndex = textLayout.getLineForOffset(cursorOffset)
613
- val selectedLineTop = textLayout.getLineTop(selectedLineIndex)
614
- val selectedLineBottom = textLayout.getLineBottom(selectedLineIndex)
615
- val visibleTextHeight = height - paddingTop - paddingBottom
733
+ val selectedLineIndex = textLayout.getLineForOffset(cursorOffset)
734
+ val selectedLineTop = textLayout.getLineTop(selectedLineIndex)
735
+ val selectedLineBottom = textLayout.getLineBottom(selectedLineIndex)
736
+ val visibleTextHeight = height - paddingTop - paddingBottom
616
737
 
617
- if (visibleTextHeight <= 0) return
738
+ if (visibleTextHeight <= 0) return
618
739
 
619
- val visibleTop = scrollY
620
- val visibleBottom = scrollY + visibleTextHeight
621
- var targetScrollY = scrollY
622
-
623
- if (selectedLineTop < visibleTop) {
624
- targetScrollY = selectedLineTop
625
- } else if (selectedLineBottom > visibleBottom) {
626
- targetScrollY = selectedLineBottom - visibleTextHeight
627
- }
740
+ val visibleTop = scrollY
741
+ val visibleBottom = scrollY + visibleTextHeight
742
+ var targetScrollY = scrollY
628
743
 
629
- val maxScrollY = (textLayout.height - visibleTextHeight).coerceAtLeast(0)
630
- targetScrollY = targetScrollY.coerceIn(0, maxScrollY)
631
- scrollTo(scrollX, targetScrollY)
744
+ if (selectedLineTop < visibleTop) {
745
+ targetScrollY = selectedLineTop
746
+ } else if (selectedLineBottom > visibleBottom) {
747
+ targetScrollY = selectedLineBottom - visibleTextHeight
632
748
  }
633
749
 
634
- private fun reApplyHtmlStyleForSpans(previousHtmlStyle: HtmlStyle, nextHtmlStyle: HtmlStyle) {
750
+ val maxScrollY = (textLayout.height - visibleTextHeight).coerceAtLeast(0)
751
+ targetScrollY = targetScrollY.coerceIn(0, maxScrollY)
752
+ scrollTo(scrollX, targetScrollY)
753
+ }
754
+
755
+ private fun reApplyHtmlStyleForSpans(
756
+ previousHtmlStyle: HtmlStyle,
757
+ nextHtmlStyle: HtmlStyle,
758
+ ) {
635
759
  val shouldRemoveBoldSpanFromH1Span = !previousHtmlStyle.h1Bold && nextHtmlStyle.h1Bold
636
760
  val shouldRemoveBoldSpanFromH2Span = !previousHtmlStyle.h2Bold && nextHtmlStyle.h2Bold
637
761
  val shouldRemoveBoldSpanFromH3Span = !previousHtmlStyle.h3Bold && nextHtmlStyle.h3Bold
@@ -652,7 +776,9 @@ class EnrichedTextInputView : AppCompatEditText {
652
776
 
653
777
  if (start == -1 || end == -1) continue
654
778
 
655
- if ((span is EnrichedH1Span && shouldRemoveBoldSpanFromH1Span) || (span is EnrichedH2Span && shouldRemoveBoldSpanFromH2Span) || (span is EnrichedH3Span && shouldRemoveBoldSpanFromH3Span)) {
779
+ if ((span is EnrichedH1Span && shouldRemoveBoldSpanFromH1Span) || (span is EnrichedH2Span && shouldRemoveBoldSpanFromH2Span) ||
780
+ (span is EnrichedH3Span && shouldRemoveBoldSpanFromH3Span)
781
+ ) {
656
782
  val isRemoved = removeStyle(EnrichedSpans.BOLD, start, end)
657
783
  if (isRemoved) shouldEmitStateChange = true
658
784
  }
@@ -673,6 +799,9 @@ class EnrichedTextInputView : AppCompatEditText {
673
799
  override fun onAttachedToWindow() {
674
800
  super.onAttachedToWindow()
675
801
 
802
+ // https://github.com/facebook/react-native/blob/36df97f500aa0aa8031098caf7526db358b6ddc1/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt#L946
803
+ super.setTextIsSelectable(true)
804
+
676
805
  if (autoFocus && !didAttachToWindow) {
677
806
  requestFocusProgrammatically()
678
807
  }
@@ -2,7 +2,9 @@ package com.swmansion.enriched
2
2
 
3
3
  import com.facebook.react.bridge.Arguments
4
4
 
5
- class EnrichedTextInputViewLayoutManager(private val view: EnrichedTextInputView) {
5
+ class EnrichedTextInputViewLayoutManager(
6
+ private val view: EnrichedTextInputView,
7
+ ) {
6
8
  private var forceHeightRecalculationCounter: Int = 0
7
9
 
8
10
  fun invalidateLayout() {