react-native-enriched 0.4.1 → 0.5.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 (53) hide show
  1. package/README.md +27 -2
  2. package/ReactNativeEnriched.podspec +5 -1
  3. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedTextInputViewManagerDelegate.java +12 -0
  4. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedTextInputViewManagerInterface.java +4 -0
  5. package/android/generated/jni/react/renderer/components/ReactNativeEnrichedSpec/EventEmitters.cpp +149 -0
  6. package/android/generated/jni/react/renderer/components/ReactNativeEnrichedSpec/EventEmitters.h +146 -0
  7. package/android/generated/jni/react/renderer/components/ReactNativeEnrichedSpec/Props.cpp +16 -1
  8. package/android/generated/jni/react/renderer/components/ReactNativeEnrichedSpec/Props.h +46 -0
  9. package/android/src/main/java/com/swmansion/enriched/common/GumboNormalizer.kt +5 -0
  10. package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedCheckboxListSpan.kt +3 -2
  11. package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedUnorderedListSpan.kt +2 -1
  12. package/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputView.kt +166 -20
  13. package/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputViewManager.kt +32 -0
  14. package/android/src/main/java/com/swmansion/enriched/textinput/MeasurementStore.kt +19 -2
  15. package/android/src/main/java/com/swmansion/enriched/textinput/events/OnContextMenuItemPressEvent.kt +35 -0
  16. package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedLineHeightSpan.kt +44 -0
  17. package/android/src/main/java/com/swmansion/enriched/textinput/styles/ParametrizedStyles.kt +16 -0
  18. package/android/src/main/java/com/swmansion/enriched/textinput/utils/EnrichedSpanState.kt +18 -12
  19. package/android/src/main/new_arch/CMakeLists.txt +9 -13
  20. package/android/src/main/new_arch/GumboNormalizerJni.cpp +14 -0
  21. package/android/src/main/new_arch/react/renderer/components/ReactNativeEnrichedSpec/conversions.h +2 -21
  22. package/cpp/CMakeLists.txt +50 -0
  23. package/cpp/GumboParser/GumboParser.h +34043 -0
  24. package/cpp/README.md +59 -0
  25. package/cpp/parser/GumboNormalizer.c +915 -0
  26. package/cpp/parser/GumboParser.cpp +16 -0
  27. package/cpp/parser/GumboParser.hpp +23 -0
  28. package/cpp/tests/GumboParserTest.cpp +457 -0
  29. package/ios/EnrichedTextInputView.h +2 -0
  30. package/ios/EnrichedTextInputView.mm +152 -2
  31. package/ios/config/InputConfig.h +3 -0
  32. package/ios/config/InputConfig.mm +15 -0
  33. package/ios/extensions/LayoutManagerExtension.mm +34 -11
  34. package/ios/generated/ReactNativeEnrichedSpec/EventEmitters.cpp +149 -0
  35. package/ios/generated/ReactNativeEnrichedSpec/EventEmitters.h +146 -0
  36. package/ios/generated/ReactNativeEnrichedSpec/Props.cpp +16 -1
  37. package/ios/generated/ReactNativeEnrichedSpec/Props.h +46 -0
  38. package/ios/generated/ReactNativeEnrichedSpec/RCTComponentViewHelpers.h +29 -0
  39. package/ios/inputParser/InputParser.mm +27 -0
  40. package/ios/inputTextView/InputTextView.mm +3 -3
  41. package/lib/module/EnrichedTextInput.js +43 -30
  42. package/lib/module/EnrichedTextInput.js.map +1 -1
  43. package/lib/module/spec/EnrichedTextInputNativeComponent.ts +118 -22
  44. package/lib/typescript/src/EnrichedTextInput.d.ts +24 -6
  45. package/lib/typescript/src/EnrichedTextInput.d.ts.map +1 -1
  46. package/lib/typescript/src/index.d.ts +1 -1
  47. package/lib/typescript/src/index.d.ts.map +1 -1
  48. package/lib/typescript/src/spec/EnrichedTextInputNativeComponent.d.ts +111 -21
  49. package/lib/typescript/src/spec/EnrichedTextInputNativeComponent.d.ts.map +1 -1
  50. package/package.json +5 -5
  51. package/src/EnrichedTextInput.tsx +79 -40
  52. package/src/index.tsx +0 -1
  53. package/src/spec/EnrichedTextInputNativeComponent.ts +118 -22
@@ -15,16 +15,18 @@ import android.util.AttributeSet
15
15
  import android.util.Log
16
16
  import android.util.Patterns
17
17
  import android.util.TypedValue
18
+ import android.view.ActionMode
18
19
  import android.view.Gravity
20
+ import android.view.Menu
21
+ import android.view.MenuItem
19
22
  import android.view.MotionEvent
20
23
  import android.view.inputmethod.EditorInfo
21
24
  import android.view.inputmethod.InputConnection
22
25
  import android.view.inputmethod.InputMethodManager
23
26
  import androidx.appcompat.widget.AppCompatEditText
24
27
  import androidx.core.view.ViewCompat
25
- import androidx.core.view.inputmethod.EditorInfoCompat
26
- import androidx.core.view.inputmethod.InputConnectionCompat
27
28
  import com.facebook.react.bridge.ReactContext
29
+ import com.facebook.react.bridge.ReadableArray
28
30
  import com.facebook.react.bridge.ReadableMap
29
31
  import com.facebook.react.common.ReactConstants
30
32
  import com.facebook.react.uimanager.PixelUtil
@@ -34,8 +36,10 @@ import com.facebook.react.views.text.ReactTypefaceUtils.applyStyles
34
36
  import com.facebook.react.views.text.ReactTypefaceUtils.parseFontStyle
35
37
  import com.facebook.react.views.text.ReactTypefaceUtils.parseFontWeight
36
38
  import com.swmansion.enriched.common.EnrichedConstants
39
+ import com.swmansion.enriched.common.GumboNormalizer
37
40
  import com.swmansion.enriched.common.parser.EnrichedParser
38
41
  import com.swmansion.enriched.textinput.events.MentionHandler
42
+ import com.swmansion.enriched.textinput.events.OnContextMenuItemPressEvent
39
43
  import com.swmansion.enriched.textinput.events.OnInputBlurEvent
40
44
  import com.swmansion.enriched.textinput.events.OnInputFocusEvent
41
45
  import com.swmansion.enriched.textinput.events.OnRequestHtmlResultEvent
@@ -46,6 +50,8 @@ import com.swmansion.enriched.textinput.spans.EnrichedInputH4Span
46
50
  import com.swmansion.enriched.textinput.spans.EnrichedInputH5Span
47
51
  import com.swmansion.enriched.textinput.spans.EnrichedInputH6Span
48
52
  import com.swmansion.enriched.textinput.spans.EnrichedInputImageSpan
53
+ import com.swmansion.enriched.textinput.spans.EnrichedInputLinkSpan
54
+ import com.swmansion.enriched.textinput.spans.EnrichedLineHeightSpan
49
55
  import com.swmansion.enriched.textinput.spans.EnrichedSpans
50
56
  import com.swmansion.enriched.textinput.spans.interfaces.EnrichedInputSpan
51
57
  import com.swmansion.enriched.textinput.styles.HtmlStyle
@@ -94,8 +100,10 @@ class EnrichedTextInputView : AppCompatEditText {
94
100
  var shouldEmitHtml: Boolean = false
95
101
  var shouldEmitOnChangeText: Boolean = false
96
102
  var experimentalSynchronousEvents: Boolean = false
103
+ var useHtmlNormalizer: Boolean = false
97
104
 
98
105
  var fontSize: Float? = null
106
+ private var lineHeight: Float? = null
99
107
  private var autoFocus = false
100
108
  private var typefaceDirty = false
101
109
  private var didAttachToWindow = false
@@ -108,6 +116,7 @@ class EnrichedTextInputView : AppCompatEditText {
108
116
 
109
117
  private var inputMethodManager: InputMethodManager? = null
110
118
  private val spannableFactory = EnrichedTextInputSpannableFactory()
119
+ private var contextMenuItems: List<Pair<Int, String>> = emptyList()
111
120
 
112
121
  constructor(context: Context) : super(context) {
113
122
  prepareComponent()
@@ -271,7 +280,7 @@ class EnrichedTextInputView : AppCompatEditText {
271
280
  val parsedText = parseText(htmlText)
272
281
  if (parsedText is Spannable) {
273
282
  val finalText = currentText.mergeSpannables(start, end, parsedText)
274
- setValue(finalText)
283
+ setValue(finalText, false)
275
284
  return
276
285
  }
277
286
  }
@@ -292,26 +301,45 @@ class EnrichedTextInputView : AppCompatEditText {
292
301
  setSelection(selection?.start ?: text?.length ?: 0)
293
302
  }
294
303
 
295
- private fun parseText(text: CharSequence): CharSequence {
296
- val isHtml = text.startsWith("<html>") && text.endsWith("</html>")
297
- if (!isHtml) return text
304
+ private fun normalizeHtmlIfNeeded(text: CharSequence): CharSequence {
305
+ if (!useHtmlNormalizer) return text
306
+ val normalized = GumboNormalizer.normalizeHtml(text.toString()) ?: return text
298
307
 
299
- try {
300
- val parsed = EnrichedParser.fromHtml(text.toString(), htmlStyle, spannableFactory)
301
- val withoutLastNewLine = parsed.trimEnd('\n')
302
- return withoutLastNewLine
308
+ return try {
309
+ val parsed = EnrichedParser.fromHtml(normalized, htmlStyle, spannableFactory)
310
+ parsed.trimEnd('\n')
303
311
  } catch (e: Exception) {
304
- Log.e("EnrichedTextInputView", "Error parsing HTML: ${e.message}")
305
- return text
312
+ Log.e(TAG, "Error parsing normalized HTML: ${e.message}")
313
+ text
314
+ }
315
+ }
316
+
317
+ private fun parseText(text: CharSequence): CharSequence {
318
+ val isInternalHtml = text.startsWith("<html>") && text.endsWith("</html>")
319
+
320
+ if (isInternalHtml) {
321
+ try {
322
+ val parsed = EnrichedParser.fromHtml(text.toString(), htmlStyle, spannableFactory)
323
+ return parsed.trimEnd('\n')
324
+ } catch (e: Exception) {
325
+ Log.e(TAG, "Error parsing HTML: ${e.message}")
326
+ return normalizeHtmlIfNeeded(text)
327
+ }
306
328
  }
329
+
330
+ return normalizeHtmlIfNeeded(text)
307
331
  }
308
332
 
309
- fun setValue(value: CharSequence?) {
333
+ fun setValue(
334
+ value: CharSequence?,
335
+ shouldParseHtml: Boolean = true,
336
+ ) {
310
337
  if (value == null) return
311
338
 
312
339
  runAsATransaction {
313
- val newText = parseText(value)
340
+ val newText = if (shouldParseHtml) parseText(value) else value
314
341
  setText(newText)
342
+ applyLineSpacing()
315
343
 
316
344
  observeAsyncImages()
317
345
 
@@ -389,8 +417,7 @@ class EnrichedTextInputView : AppCompatEditText {
389
417
 
390
418
  fun setCursorColor(colorInt: Int?) {
391
419
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
392
- val cursorDrawable = textCursorDrawable
393
- if (cursorDrawable == null) return
420
+ val cursorDrawable = textCursorDrawable ?: return
394
421
 
395
422
  if (colorInt != null) {
396
423
  cursorDrawable.colorFilter = BlendModeColorFilter(colorInt, BlendMode.SRC_IN)
@@ -424,6 +451,28 @@ class EnrichedTextInputView : AppCompatEditText {
424
451
  forceScrollToSelection()
425
452
  }
426
453
 
454
+ fun setLineHeight(height: Float) {
455
+ lineHeight = if (height == 0f) null else height
456
+ applyLineSpacing()
457
+ layoutManager.invalidateLayout()
458
+ forceScrollToSelection()
459
+ }
460
+
461
+ private fun applyLineSpacing() {
462
+ val spannable = text as? Spannable ?: return
463
+ spannable
464
+ .getSpans(0, spannable.length, EnrichedLineHeightSpan::class.java)
465
+ .forEach { spannable.removeSpan(it) }
466
+
467
+ val lh = lineHeight ?: return
468
+ spannable.setSpan(
469
+ EnrichedLineHeightSpan(lh),
470
+ 0,
471
+ spannable.length,
472
+ Spannable.SPAN_INCLUSIVE_INCLUSIVE,
473
+ )
474
+ }
475
+
427
476
  fun setFontFamily(family: String?) {
428
477
  if (family != fontFamily) {
429
478
  fontFamily = family
@@ -490,12 +539,100 @@ class EnrichedTextInputView : AppCompatEditText {
490
539
 
491
540
  try {
492
541
  linkRegex = Pattern.compile("(?s).*?($patternStr).*", flags)
493
- } catch (e: PatternSyntaxException) {
494
- Log.w("EnrichedTextInputView", "Invalid link regex pattern: $patternStr")
542
+ } catch (_: PatternSyntaxException) {
543
+ Log.w(TAG, "Invalid link regex pattern: $patternStr")
495
544
  linkRegex = Patterns.WEB_URL
496
545
  }
497
546
  }
498
547
 
548
+ fun setContextMenuItems(items: ReadableArray?) {
549
+ if (items == null) {
550
+ contextMenuItems = emptyList()
551
+ return
552
+ }
553
+
554
+ val result = mutableListOf<Pair<Int, String>>()
555
+ for (i in 0 until items.size()) {
556
+ val item = items.getMap(i) ?: continue
557
+ val text = item.getString("text") ?: continue
558
+ result.add(Pair(i, text))
559
+ }
560
+
561
+ contextMenuItems = result
562
+ }
563
+
564
+ override fun startActionMode(
565
+ callback: ActionMode.Callback?,
566
+ type: Int,
567
+ ): ActionMode? {
568
+ if (contextMenuItems.isEmpty()) {
569
+ return super.startActionMode(callback, type)
570
+ }
571
+
572
+ val wrappedCallback =
573
+ object : ActionMode.Callback2() {
574
+ override fun onCreateActionMode(
575
+ mode: ActionMode,
576
+ menu: Menu,
577
+ ): Boolean {
578
+ val result = callback?.onCreateActionMode(mode, menu) ?: false
579
+ for ((index, text) in contextMenuItems) {
580
+ menu.add(Menu.NONE, CONTEXT_MENU_ITEM_ID + index, Menu.NONE, text)
581
+ }
582
+
583
+ return result
584
+ }
585
+
586
+ override fun onPrepareActionMode(
587
+ mode: ActionMode,
588
+ menu: Menu,
589
+ ) = callback?.onPrepareActionMode(mode, menu) ?: false
590
+
591
+ override fun onActionItemClicked(
592
+ mode: ActionMode,
593
+ menuItem: MenuItem,
594
+ ): Boolean {
595
+ val itemId = menuItem.itemId
596
+ if (itemId < CONTEXT_MENU_ITEM_ID) {
597
+ return callback?.onActionItemClicked(mode, menuItem) ?: false
598
+ }
599
+
600
+ val selStart = selection?.start ?: 0
601
+ val selEnd = selection?.end ?: 0
602
+ val itemText = contextMenuItems.getOrNull(itemId - CONTEXT_MENU_ITEM_ID)?.second ?: return false
603
+ emitContextMenuItemPressEvent(itemText)
604
+ mode.finish()
605
+ post {
606
+ // Ensures selection is not lost after the action mode is finished
607
+ if (selStart in 0..selEnd) {
608
+ setSelection(selStart, selEnd)
609
+ }
610
+ }
611
+ return true
612
+ }
613
+
614
+ override fun onDestroyActionMode(mode: ActionMode) {
615
+ callback?.onDestroyActionMode(mode)
616
+ }
617
+ }
618
+
619
+ return super.startActionMode(wrappedCallback, type)
620
+ }
621
+
622
+ private fun emitContextMenuItemPressEvent(itemText: String) {
623
+ val start = selection?.start ?: return
624
+ val end = selection.end
625
+ val styleState = spanState?.getStyleStatePayload() ?: return
626
+ val selectedText = text?.subSequence(start, end)?.toString() ?: ""
627
+
628
+ val reactContext = context as ReactContext
629
+ val surfaceId = UIManagerHelper.getSurfaceId(reactContext)
630
+ val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, id)
631
+ dispatcher?.dispatchEvent(
632
+ OnContextMenuItemPressEvent(surfaceId, id, itemText, selectedText, start, end, styleState, experimentalSynchronousEvents),
633
+ )
634
+ }
635
+
499
636
  // https://github.com/facebook/react-native/blob/36df97f500aa0aa8031098caf7526db358b6ddc1/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt#L283C2-L284C1
500
637
  // After the text changes inside an EditText, TextView checks if a layout() has been requested.
501
638
  // If it has, it will not scroll the text to the end of the new text inserted, but wait for the
@@ -550,7 +687,7 @@ class EnrichedTextInputView : AppCompatEditText {
550
687
  EnrichedSpans.ORDERED_LIST -> listStyles?.toggleStyle(EnrichedSpans.ORDERED_LIST)
551
688
  EnrichedSpans.UNORDERED_LIST -> listStyles?.toggleStyle(EnrichedSpans.UNORDERED_LIST)
552
689
  EnrichedSpans.CHECKBOX_LIST -> listStyles?.toggleStyle(EnrichedSpans.CHECKBOX_LIST)
553
- else -> Log.w("EnrichedTextInputView", "Unknown style: $name")
690
+ else -> Log.w(TAG, "Unknown style: $name")
554
691
  }
555
692
 
556
693
  layoutManager.invalidateLayout()
@@ -685,6 +822,13 @@ class EnrichedTextInputView : AppCompatEditText {
685
822
  parametrizedStyles?.setLinkSpan(start, end, text, url)
686
823
  }
687
824
 
825
+ fun removeLink(
826
+ start: Int,
827
+ end: Int,
828
+ ) {
829
+ parametrizedStyles?.removeLinkSpans(start, end)
830
+ }
831
+
688
832
  fun addImage(
689
833
  src: String,
690
834
  width: Float,
@@ -719,7 +863,7 @@ class EnrichedTextInputView : AppCompatEditText {
719
863
  val html =
720
864
  try {
721
865
  EnrichedParser.toHtmlWithDefault(text)
722
- } catch (e: Exception) {
866
+ } catch (_: Exception) {
723
867
  null
724
868
  }
725
869
 
@@ -846,6 +990,8 @@ class EnrichedTextInputView : AppCompatEditText {
846
990
  }
847
991
 
848
992
  companion object {
993
+ const val TAG = "EnrichedTextInputView"
849
994
  const val CLIPBOARD_TAG = "react-native-enriched-clipboard"
995
+ private const val CONTEXT_MENU_ITEM_ID = 10000
850
996
  }
851
997
  }
@@ -19,6 +19,7 @@ import com.swmansion.enriched.textinput.events.OnChangeHtmlEvent
19
19
  import com.swmansion.enriched.textinput.events.OnChangeSelectionEvent
20
20
  import com.swmansion.enriched.textinput.events.OnChangeStateEvent
21
21
  import com.swmansion.enriched.textinput.events.OnChangeTextEvent
22
+ import com.swmansion.enriched.textinput.events.OnContextMenuItemPressEvent
22
23
  import com.swmansion.enriched.textinput.events.OnInputBlurEvent
23
24
  import com.swmansion.enriched.textinput.events.OnInputFocusEvent
24
25
  import com.swmansion.enriched.textinput.events.OnInputKeyPressEvent
@@ -72,6 +73,7 @@ class EnrichedTextInputViewManager :
72
73
  map.put(OnRequestHtmlResultEvent.EVENT_NAME, mapOf("registrationName" to OnRequestHtmlResultEvent.EVENT_NAME))
73
74
  map.put(OnInputKeyPressEvent.EVENT_NAME, mapOf("registrationName" to OnInputKeyPressEvent.EVENT_NAME))
74
75
  map.put(OnPasteImagesEvent.EVENT_NAME, mapOf("registrationName" to OnPasteImagesEvent.EVENT_NAME))
76
+ map.put(OnContextMenuItemPressEvent.EVENT_NAME, mapOf("registrationName" to OnContextMenuItemPressEvent.EVENT_NAME))
75
77
 
76
78
  return map
77
79
  }
@@ -173,6 +175,14 @@ class EnrichedTextInputViewManager :
173
175
  view?.setFontSize(size)
174
176
  }
175
177
 
178
+ @ReactProp(name = "lineHeight", defaultFloat = 0f)
179
+ override fun setLineHeight(
180
+ view: EnrichedTextInputView?,
181
+ height: Float,
182
+ ) {
183
+ view?.setLineHeight(height)
184
+ }
185
+
176
186
  @ReactProp(name = "fontFamily")
177
187
  override fun setFontFamily(
178
188
  view: EnrichedTextInputView?,
@@ -257,6 +267,20 @@ class EnrichedTextInputViewManager :
257
267
  view?.experimentalSynchronousEvents = value
258
268
  }
259
269
 
270
+ override fun setContextMenuItems(
271
+ view: EnrichedTextInputView?,
272
+ value: ReadableArray?,
273
+ ) {
274
+ view?.setContextMenuItems(value)
275
+ }
276
+
277
+ override fun setUseHtmlNormalizer(
278
+ view: EnrichedTextInputView?,
279
+ value: Boolean,
280
+ ) {
281
+ view?.useHtmlNormalizer = value
282
+ }
283
+
260
284
  override fun focus(view: EnrichedTextInputView?) {
261
285
  view?.requestFocusProgrammatically()
262
286
  }
@@ -357,6 +381,14 @@ class EnrichedTextInputViewManager :
357
381
  view?.addLink(start, end, text, url)
358
382
  }
359
383
 
384
+ override fun removeLink(
385
+ view: EnrichedTextInputView?,
386
+ start: Int,
387
+ end: Int,
388
+ ) {
389
+ view?.removeLink(start, end)
390
+ }
391
+
360
392
  override fun addImage(
361
393
  view: EnrichedTextInputView?,
362
394
  src: String,
@@ -5,6 +5,7 @@ import android.graphics.Typeface
5
5
  import android.graphics.text.LineBreaker
6
6
  import android.os.Build
7
7
  import android.text.Spannable
8
+ import android.text.SpannableString
8
9
  import android.text.StaticLayout
9
10
  import android.text.TextPaint
10
11
  import android.util.Log
@@ -16,6 +17,7 @@ import com.facebook.react.views.text.ReactTypefaceUtils.parseFontWeight
16
17
  import com.facebook.yoga.YogaMeasureMode
17
18
  import com.facebook.yoga.YogaMeasureOutput
18
19
  import com.swmansion.enriched.common.parser.EnrichedParser
20
+ import com.swmansion.enriched.textinput.spans.EnrichedLineHeightSpan
19
21
  import com.swmansion.enriched.textinput.styles.HtmlStyle
20
22
  import java.util.concurrent.ConcurrentHashMap
21
23
  import kotlin.math.ceil
@@ -141,16 +143,31 @@ object MeasurementStore {
141
143
  ): Long {
142
144
  val defaultView = EnrichedTextInputView(context)
143
145
 
144
- val text = getInitialText(defaultView, props)
146
+ val rawText = getInitialText(defaultView, props)
145
147
  val fontSize = getInitialFontSize(defaultView, props)
148
+ val lineHeight = props?.getDouble("lineHeight")?.toFloat() ?: 0f
146
149
 
147
150
  val fontFamily = props?.getString("fontFamily")
148
151
  val fontStyle = parseFontStyle(props?.getString("fontStyle"))
149
152
  val fontWeight = parseFontWeight(props?.getString("fontWeight"))
150
153
 
154
+ val text: CharSequence =
155
+ if (lineHeight > 0f) {
156
+ val spannable = SpannableString(rawText)
157
+ spannable.setSpan(
158
+ EnrichedLineHeightSpan(lineHeight),
159
+ 0,
160
+ spannable.length,
161
+ Spannable.SPAN_INCLUSIVE_INCLUSIVE,
162
+ )
163
+ spannable
164
+ } else {
165
+ rawText
166
+ }
167
+
151
168
  val typeface = applyStyles(defaultView.typeface, fontStyle, fontWeight, fontFamily, context.assets)
152
169
  val paintParams = PaintParams(typeface, fontSize)
153
- val size = measure(width, text, PaintParams(typeface, fontSize))
170
+ val size = measure(width, text, paintParams)
154
171
 
155
172
  if (id != null) {
156
173
  data[id] = MeasurementParams(true, width, size, text, paintParams)
@@ -0,0 +1,35 @@
1
+ package com.swmansion.enriched.textinput.events
2
+
3
+ import android.util.Log
4
+ import com.facebook.react.bridge.Arguments
5
+ import com.facebook.react.bridge.WritableMap
6
+ import com.facebook.react.uimanager.events.Event
7
+
8
+ class OnContextMenuItemPressEvent(
9
+ surfaceId: Int,
10
+ viewId: Int,
11
+ private val itemText: String,
12
+ private val selectedText: String,
13
+ private val selectionStart: Int,
14
+ private val selectionEnd: Int,
15
+ private val styleState: WritableMap,
16
+ private val experimentalSynchronousEvents: Boolean,
17
+ ) : Event<OnContextMenuItemPressEvent>(surfaceId, viewId) {
18
+ override fun getEventName(): String = EVENT_NAME
19
+
20
+ override fun getEventData(): WritableMap {
21
+ val eventData: WritableMap = Arguments.createMap()
22
+ eventData.putString("itemText", itemText)
23
+ eventData.putString("selectedText", selectedText)
24
+ eventData.putInt("selectionStart", selectionStart)
25
+ eventData.putInt("selectionEnd", selectionEnd)
26
+ eventData.putMap("styleState", styleState)
27
+ return eventData
28
+ }
29
+
30
+ override fun experimental_isSynchronous(): Boolean = experimentalSynchronousEvents
31
+
32
+ companion object {
33
+ const val EVENT_NAME: String = "onContextMenuItemPress"
34
+ }
35
+ }
@@ -0,0 +1,44 @@
1
+ package com.swmansion.enriched.textinput.spans
2
+
3
+ import android.graphics.Paint
4
+ import android.text.Spannable
5
+ import android.text.TextPaint
6
+ import android.text.style.LineHeightSpan
7
+ import android.text.style.MetricAffectingSpan
8
+ import com.facebook.react.uimanager.PixelUtil
9
+ import com.swmansion.enriched.common.spans.interfaces.EnrichedHeadingSpan
10
+
11
+ class EnrichedLineHeightSpan(
12
+ val lineHeight: Float,
13
+ ) : MetricAffectingSpan(),
14
+ LineHeightSpan {
15
+ override fun updateDrawState(p0: TextPaint?) {
16
+ // Do nothing but inform TextView that line height should be recalculated
17
+ }
18
+
19
+ override fun updateMeasureState(p0: TextPaint) {
20
+ // Do nothing but inform TextView that line height should be recalculated
21
+ }
22
+
23
+ override fun chooseHeight(
24
+ text: CharSequence,
25
+ start: Int,
26
+ end: Int,
27
+ spanstartv: Int,
28
+ v: Int,
29
+ fm: Paint.FontMetricsInt,
30
+ ) {
31
+ val spannable = text as? Spannable ?: return
32
+ // Do not modify line height for headings
33
+ // In the future we may consider adding custom lineHeight support for each paragraph style
34
+ if (spannable.getSpans(start, end, EnrichedHeadingSpan::class.java).isNotEmpty()) return
35
+
36
+ val lineHeightPx = PixelUtil.toPixelFromSP(lineHeight)
37
+ val currentHeight = (fm.descent - fm.ascent).toFloat()
38
+ if (lineHeightPx <= currentHeight) return
39
+
40
+ val extra = (lineHeightPx - currentHeight).toInt()
41
+ fm.ascent -= extra
42
+ fm.top = minOf(fm.top, fm.ascent)
43
+ }
44
+ }
@@ -69,6 +69,22 @@ class ParametrizedStyles(
69
69
  isSettingLinkSpan = false
70
70
  }
71
71
 
72
+ fun removeLinkSpans(
73
+ start: Int,
74
+ end: Int,
75
+ ) {
76
+ val spannable = view.text as SpannableStringBuilder
77
+ val textLength = spannable.length
78
+ val clampedStart = minOf(start, end).coerceIn(0, textLength)
79
+ val clampedEnd = maxOf(start, end).coerceIn(0, textLength)
80
+
81
+ val spans = spannable.getSpans(clampedStart, clampedEnd, EnrichedInputLinkSpan::class.java)
82
+ for (span in spans) {
83
+ spannable.removeSpan(span)
84
+ }
85
+ view.selection?.validateStyles()
86
+ }
87
+
72
88
  fun afterTextChanged(
73
89
  s: Editable,
74
90
  startCursorPosition: Int,
@@ -203,18 +203,7 @@ class EnrichedSpanState(
203
203
  }
204
204
  }
205
205
 
206
- private fun emitStateChangeEvent() {
207
- val context = view.context as ReactContext
208
- val surfaceId = UIManagerHelper.getSurfaceId(context)
209
- val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(context, view.id)
210
-
211
- dispatchPayload(dispatcher, surfaceId)
212
- }
213
-
214
- private fun dispatchPayload(
215
- dispatcher: EventDispatcher?,
216
- surfaceId: Int,
217
- ) {
206
+ fun getStyleStatePayload(): WritableMap {
218
207
  val activeStyles =
219
208
  listOfNotNull(
220
209
  if (boldStart != null) EnrichedSpans.BOLD else null,
@@ -258,6 +247,23 @@ class EnrichedSpanState(
258
247
  payload.putMap("mention", getStyleState(activeStyles, EnrichedSpans.MENTION))
259
248
  payload.putMap("checkboxList", getStyleState(activeStyles, EnrichedSpans.CHECKBOX_LIST))
260
249
 
250
+ return payload
251
+ }
252
+
253
+ private fun emitStateChangeEvent() {
254
+ val context = view.context as ReactContext
255
+ val surfaceId = UIManagerHelper.getSurfaceId(context)
256
+ val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(context, view.id)
257
+
258
+ dispatchPayload(dispatcher, surfaceId)
259
+ }
260
+
261
+ private fun dispatchPayload(
262
+ dispatcher: EventDispatcher?,
263
+ surfaceId: Int,
264
+ ) {
265
+ val payload = getStyleStatePayload()
266
+
261
267
  // Do not emit event if payload is the same
262
268
  if (previousPayload == payload) {
263
269
  return
@@ -8,8 +8,13 @@ set(LIB_ANDROID_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../..)
8
8
  set(LIB_ANDROID_GENERATED_JNI_DIR ${LIB_ANDROID_DIR}/generated/jni)
9
9
  set(LIB_ANDROID_GENERATED_COMPONENTS_DIR ${LIB_ANDROID_GENERATED_JNI_DIR}/react/renderer/components/${LIB_LITERAL})
10
10
 
11
+ set(LIB_CPP_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../../../cpp)
12
+
11
13
  file(GLOB LIB_MODULE_SRCS CONFIGURE_DEPENDS *.cpp react/renderer/components/${LIB_LITERAL}/*.cpp)
12
14
  file(GLOB LIB_CODEGEN_SRCS CONFIGURE_DEPENDS ${LIB_ANDROID_GENERATED_COMPONENTS_DIR}/*.cpp)
15
+ file(GLOB LIB_CPP_SRCS CONFIGURE_DEPENDS ${LIB_CPP_DIR}/parser/GumboParser.cpp ${LIB_CPP_DIR}/parser/GumboNormalizer.c)
16
+
17
+ set_source_files_properties(${LIB_CPP_DIR}/parser/GumboNormalizer.c PROPERTIES LANGUAGE C COMPILE_FLAGS "-std=c99")
13
18
 
14
19
  if(NOT DEFINED REACT_NATIVE_MINOR_VERSION)
15
20
  set(REACT_NATIVE_MINOR_VERSION ${ReactAndroid_VERSION_MINOR})
@@ -23,6 +28,7 @@ add_library(
23
28
  ${LIB_MODULE_SRCS}
24
29
  ${LIB_CUSTOM_SRCS}
25
30
  ${LIB_CODEGEN_SRCS}
31
+ ${LIB_CPP_SRCS}
26
32
  )
27
33
 
28
34
  target_include_directories(
@@ -33,6 +39,8 @@ target_include_directories(
33
39
  ${LIB_ANDROID_GENERATED_JNI_DIR}
34
40
  ${LIB_ANDROID_GENERATED_COMPONENTS_DIR}
35
41
  ${LIB_CPP_DIR}
42
+ ${LIB_CPP_DIR}/parser
43
+ ${LIB_CPP_DIR}/GumboParser
36
44
  )
37
45
 
38
46
  find_package(fbjni REQUIRED CONFIG)
@@ -45,19 +53,7 @@ target_link_libraries(
45
53
  ReactAndroid::reactnative
46
54
  )
47
55
 
48
- if(ReactAndroid_VERSION_MINOR GREATER_EQUAL 80)
49
- target_compile_reactnative_options(${LIB_TARGET_NAME} PRIVATE)
50
- else()
51
- target_compile_options(
52
- ${LIB_TARGET_NAME}
53
- PRIVATE
54
- -DLOG_TAG=\"ReactNative\"
55
- -fexceptions
56
- -frtti
57
- -Wall
58
- -std=c++20
59
- )
60
- endif()
56
+ target_compile_reactnative_options(${LIB_TARGET_NAME} PRIVATE)
61
57
 
62
58
  target_include_directories(
63
59
  ${CMAKE_PROJECT_NAME}
@@ -0,0 +1,14 @@
1
+ #include "GumboParser.hpp"
2
+ #include <jni.h>
3
+ #include <string>
4
+
5
+ extern "C" JNIEXPORT jstring JNICALL
6
+ Java_com_swmansion_enriched_common_GumboNormalizer_normalizeHtml(
7
+ JNIEnv *env, jclass /*cls*/, jstring htmlJString) {
8
+ const char *htmlChars = env->GetStringUTFChars(htmlJString, nullptr);
9
+ std::string result = GumboParser::normalizeHtml(htmlChars);
10
+ env->ReleaseStringUTFChars(htmlJString, htmlChars);
11
+ if (result.empty())
12
+ return nullptr;
13
+ return env->NewStringUTF(result.c_str());
14
+ }
@@ -1,15 +1,10 @@
1
1
  #pragma once
2
2
 
3
3
  #include <folly/dynamic.h>
4
+ #include <react/renderer/components/FBReactNativeSpec/Props.h>
4
5
  #include <react/renderer/components/ReactNativeEnrichedSpec/Props.h>
5
6
  #include <react/renderer/core/propsConversions.h>
6
7
 
7
- #if REACT_NATIVE_MINOR_VERSION >= 81
8
- #include <react/renderer/components/FBReactNativeSpec/Props.h>
9
- #else
10
- #include <react/renderer/components/rncore/Props.h>
11
- #endif
12
-
13
8
  namespace facebook::react {
14
9
 
15
10
  #ifdef RN_SERIALIZABLE_STATE
@@ -22,25 +17,11 @@ inline folly::dynamic toDynamic(const EnrichedTextInputViewProps &props) {
22
17
  serializedProps["fontWeight"] = props.fontWeight;
23
18
  serializedProps["fontStyle"] = props.fontStyle;
24
19
  serializedProps["fontFamily"] = props.fontFamily;
20
+ serializedProps["lineHeight"] = props.lineHeight;
25
21
  serializedProps["htmlStyle"] = toDynamic(props.htmlStyle);
26
22
 
27
23
  return serializedProps;
28
24
  }
29
- #elif REACT_NATIVE_MINOR_VERSION >= 79
30
- inline folly::dynamic toDynamic(const EnrichedTextInputViewProps &props) {
31
- folly::dynamic serializedProps = folly::dynamic::object();
32
- serializedProps["defaultValue"] = props.defaultValue;
33
- serializedProps["placeholder"] = props.placeholder;
34
- serializedProps["fontSize"] = props.fontSize;
35
- serializedProps["fontWeight"] = props.fontWeight;
36
- serializedProps["fontStyle"] = props.fontStyle;
37
- serializedProps["fontFamily"] = props.fontFamily;
38
- // Ideally we should also serialize htmlStyle, but toDynamic function is not
39
- // generated in this RN version
40
- // As RN 0.79 and 0.80 is no longer supported, we can skip it for now
41
-
42
- return serializedProps;
43
- }
44
25
  #endif
45
26
 
46
27
  } // namespace facebook::react