react-native-enriched 0.1.5 → 0.1.6

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 (38) hide show
  1. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedTextInputViewManagerDelegate.java +3 -0
  2. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedTextInputViewManagerInterface.java +1 -0
  3. package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/Props.cpp +5 -0
  4. package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/Props.h +1 -0
  5. package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputView.kt +34 -10
  6. package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewLayoutManager.kt +7 -56
  7. package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewManager.kt +16 -19
  8. package/android/src/main/java/com/swmansion/enriched/MeasurementStore.kt +158 -0
  9. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedSpans.kt +61 -46
  10. package/android/src/main/java/com/swmansion/enriched/styles/ParametrizedStyles.kt +18 -2
  11. package/android/src/main/java/com/swmansion/enriched/utils/EnrichedParser.java +23 -3
  12. package/android/src/main/java/com/swmansion/enriched/watchers/EnrichedSpanWatcher.kt +0 -1
  13. package/android/src/main/java/com/swmansion/enriched/watchers/EnrichedTextWatcher.kt +1 -1
  14. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputMeasurementManager.cpp +15 -2
  15. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputMeasurementManager.h +1 -0
  16. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputShadowNode.cpp +1 -2
  17. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/conversions.h +27 -0
  18. package/ios/EnrichedTextInputView.h +0 -1
  19. package/ios/EnrichedTextInputView.mm +71 -40
  20. package/ios/generated/RNEnrichedTextInputViewSpec/Props.cpp +5 -0
  21. package/ios/generated/RNEnrichedTextInputViewSpec/Props.h +1 -0
  22. package/ios/inputParser/InputParser.mm +32 -7
  23. package/ios/inputTextView/InputTextView.mm +3 -5
  24. package/ios/styles/LinkStyle.mm +7 -7
  25. package/ios/styles/OrderedListStyle.mm +3 -8
  26. package/ios/styles/UnorderedListStyle.mm +3 -8
  27. package/ios/utils/StringExtension.h +1 -1
  28. package/ios/utils/StringExtension.mm +17 -8
  29. package/lib/module/EnrichedTextInput.js +2 -0
  30. package/lib/module/EnrichedTextInput.js.map +1 -1
  31. package/lib/module/EnrichedTextInputNativeComponent.ts +1 -0
  32. package/lib/typescript/src/EnrichedTextInput.d.ts +2 -1
  33. package/lib/typescript/src/EnrichedTextInput.d.ts.map +1 -1
  34. package/lib/typescript/src/EnrichedTextInputNativeComponent.d.ts +1 -0
  35. package/lib/typescript/src/EnrichedTextInputNativeComponent.d.ts.map +1 -1
  36. package/package.json +1 -1
  37. package/src/EnrichedTextInput.tsx +3 -0
  38. package/src/EnrichedTextInputNativeComponent.ts +1 -0
@@ -55,6 +55,9 @@ public class EnrichedTextInputViewManagerDelegate<T extends View, U extends Base
55
55
  case "htmlStyle":
56
56
  mViewManager.setHtmlStyle(view, (ReadableMap) value);
57
57
  break;
58
+ case "scrollEnabled":
59
+ mViewManager.setScrollEnabled(view, value == null ? false : (boolean) value);
60
+ break;
58
61
  case "color":
59
62
  mViewManager.setColor(view, ColorPropConverter.getColor(value, view.getContext()));
60
63
  break;
@@ -26,6 +26,7 @@ public interface EnrichedTextInputViewManagerInterface<T extends View> extends V
26
26
  void setSelectionColor(T view, @Nullable Integer value);
27
27
  void setAutoCapitalize(T view, @Nullable String value);
28
28
  void setHtmlStyle(T view, @Nullable ReadableMap value);
29
+ void setScrollEnabled(T view, boolean value);
29
30
  void setColor(T view, @Nullable Integer value);
30
31
  void setFontSize(T view, float value);
31
32
  void setFontFamily(T view, @Nullable String value);
@@ -30,6 +30,7 @@ EnrichedTextInputViewProps::EnrichedTextInputViewProps(
30
30
  selectionColor(convertRawProp(context, rawProps, "selectionColor", sourceProps.selectionColor, {})),
31
31
  autoCapitalize(convertRawProp(context, rawProps, "autoCapitalize", sourceProps.autoCapitalize, {})),
32
32
  htmlStyle(convertRawProp(context, rawProps, "htmlStyle", sourceProps.htmlStyle, {})),
33
+ scrollEnabled(convertRawProp(context, rawProps, "scrollEnabled", sourceProps.scrollEnabled, {false})),
33
34
  color(convertRawProp(context, rawProps, "color", sourceProps.color, {})),
34
35
  fontSize(convertRawProp(context, rawProps, "fontSize", sourceProps.fontSize, {0.0})),
35
36
  fontFamily(convertRawProp(context, rawProps, "fontFamily", sourceProps.fontFamily, {})),
@@ -94,6 +95,10 @@ folly::dynamic EnrichedTextInputViewProps::getDiffProps(
94
95
  result["htmlStyle"] = toDynamic(htmlStyle);
95
96
  }
96
97
 
98
+ if (scrollEnabled != oldProps->scrollEnabled) {
99
+ result["scrollEnabled"] = scrollEnabled;
100
+ }
101
+
97
102
  if (color != oldProps->color) {
98
103
  result["color"] = *color;
99
104
  }
@@ -559,6 +559,7 @@ class EnrichedTextInputViewProps final : public ViewProps {
559
559
  SharedColor selectionColor{};
560
560
  std::string autoCapitalize{};
561
561
  EnrichedTextInputViewHtmlStyleStruct htmlStyle{};
562
+ bool scrollEnabled{false};
562
563
  SharedColor color{};
563
564
  Float fontSize{0.0};
564
565
  std::string fontFamily{};
@@ -54,6 +54,7 @@ class EnrichedTextInputView : AppCompatEditText {
54
54
  val parametrizedStyles: ParametrizedStyles? = ParametrizedStyles(this)
55
55
  var isDuringTransaction: Boolean = false
56
56
  var isRemovingMany: Boolean = false
57
+ var scrollEnabled: Boolean = true
57
58
 
58
59
  val mentionHandler: MentionHandler? = MentionHandler(this)
59
60
  var htmlStyle: HtmlStyle = HtmlStyle(this, null)
@@ -70,6 +71,8 @@ class EnrichedTextInputView : AppCompatEditText {
70
71
  private var fontFamily: String? = null
71
72
  private var fontStyle: Int = ReactConstants.UNSET
72
73
  private var fontWeight: Int = ReactConstants.UNSET
74
+ private var defaultValue: CharSequence? = null
75
+ private var defaultValueDirty: Boolean = false
73
76
 
74
77
  private var inputMethodManager: InputMethodManager? = null
75
78
 
@@ -137,6 +140,14 @@ class EnrichedTextInputView : AppCompatEditText {
137
140
  return super.onTouchEvent(ev)
138
141
  }
139
142
 
143
+ override fun canScrollVertically(direction: Int): Boolean {
144
+ return scrollEnabled
145
+ }
146
+
147
+ override fun canScrollHorizontally(direction: Int): Boolean {
148
+ return scrollEnabled
149
+ }
150
+
140
151
  override fun onSelectionChanged(selStart: Int, selEnd: Int) {
141
152
  super.onSelectionChanged(selStart, selEnd)
142
153
  selection?.onSelection(selStart, selEnd)
@@ -306,7 +317,7 @@ class EnrichedTextInputView : AppCompatEditText {
306
317
 
307
318
  // This ensured that newly created spans will take the new font size into account
308
319
  htmlStyle.invalidateStyles()
309
- layoutManager.invalidateLayout(text)
320
+ layoutManager.invalidateLayout()
310
321
  }
311
322
 
312
323
  fun setFontFamily(family: String?) {
@@ -360,7 +371,24 @@ class EnrichedTextInputView : AppCompatEditText {
360
371
  return false
361
372
  }
362
373
 
363
- fun updateTypeface() {
374
+ fun afterUpdateTransaction() {
375
+ updateTypeface()
376
+ updateDefaultValue()
377
+ }
378
+
379
+ fun setDefaultValue(value: CharSequence?) {
380
+ defaultValue = value
381
+ defaultValueDirty = true
382
+ }
383
+
384
+ private fun updateDefaultValue() {
385
+ if (!defaultValueDirty) return
386
+
387
+ defaultValueDirty = false
388
+ setValue(defaultValue ?: "")
389
+ }
390
+
391
+ private fun updateTypeface() {
364
392
  if (!typefaceDirty) return
365
393
  typefaceDirty = false
366
394
 
@@ -368,7 +396,7 @@ class EnrichedTextInputView : AppCompatEditText {
368
396
  typeface = newTypeface
369
397
  paint.typeface = newTypeface
370
398
 
371
- layoutManager.invalidateLayout(text)
399
+ layoutManager.invalidateLayout()
372
400
  }
373
401
 
374
402
  private fun toggleStyle(name: String) {
@@ -388,7 +416,7 @@ class EnrichedTextInputView : AppCompatEditText {
388
416
  else -> Log.w("EnrichedTextInputView", "Unknown style: $name")
389
417
  }
390
418
 
391
- layoutManager.invalidateLayout(text)
419
+ layoutManager.invalidateLayout()
392
420
  }
393
421
 
394
422
  private fun removeStyle(name: String, start: Int, end: Int): Boolean {
@@ -438,7 +466,7 @@ class EnrichedTextInputView : AppCompatEditText {
438
466
  }
439
467
 
440
468
  private fun verifyStyle(name: String): Boolean {
441
- val mergingConfig = EnrichedSpans.mergingConfig[name] ?: return true
469
+ val mergingConfig = EnrichedSpans.getMergingConfigForStyle(name, htmlStyle) ?: return true
442
470
  val conflictingStyles = mergingConfig.conflictingStyles
443
471
  val blockingStyles = mergingConfig.blockingStyles
444
472
  val isEnabling = spanState?.getStart(name) == null
@@ -504,6 +532,7 @@ class EnrichedTextInputView : AppCompatEditText {
504
532
  if (!isValid) return
505
533
 
506
534
  parametrizedStyles?.setImageSpan(src)
535
+ layoutManager.invalidateLayout()
507
536
  }
508
537
 
509
538
  fun startMention(indicator: String) {
@@ -542,11 +571,6 @@ class EnrichedTextInputView : AppCompatEditText {
542
571
  didAttachToWindow = true
543
572
  }
544
573
 
545
- override fun onDetachedFromWindow() {
546
- layoutManager.cleanup()
547
- super.onDetachedFromWindow()
548
- }
549
-
550
574
  companion object {
551
575
  const val CLIPBOARD_TAG = "react-native-enriched-clipboard"
552
576
  }
@@ -1,24 +1,16 @@
1
1
  package com.swmansion.enriched
2
2
 
3
- import android.graphics.text.LineBreaker
4
- import android.os.Build
5
- import android.text.Editable
6
- import android.text.StaticLayout
7
3
  import com.facebook.react.bridge.Arguments
8
- import com.facebook.react.uimanager.PixelUtil
9
4
 
10
5
  class EnrichedTextInputViewLayoutManager(private val view: EnrichedTextInputView) {
11
- private var cachedSize: Pair<Float, Float> = Pair(0f, 0f)
12
- private var cachedYogaWidth: Float = 0f
13
6
  private var forceHeightRecalculationCounter: Int = 0
14
7
 
15
- fun cleanup() {
16
- forceHeightRecalculationCounter = 0
17
- }
8
+ fun invalidateLayout() {
9
+ val text = view.text
10
+ val paint = view.paint
18
11
 
19
- // Update shadow node's state in order to recalculate layout
20
- fun invalidateLayout(text: Editable?) {
21
- measureSize(text ?: "")
12
+ val needUpdate = MeasurementStore.store(view.id, text, paint)
13
+ if (!needUpdate) return
22
14
 
23
15
  val counter = forceHeightRecalculationCounter
24
16
  forceHeightRecalculationCounter++
@@ -27,48 +19,7 @@ class EnrichedTextInputViewLayoutManager(private val view: EnrichedTextInputView
27
19
  view.stateWrapper?.updateState(state)
28
20
  }
29
21
 
30
- fun getMeasuredSize(maxWidth: Float): Pair<Float, Float> {
31
- if (maxWidth == cachedYogaWidth) {
32
- return cachedSize
33
- }
34
-
35
- val text = view.text ?: ""
36
- val result = measureAndCacheSize(text, maxWidth)
37
- cachedYogaWidth = maxWidth
38
- return result
39
- }
40
-
41
- fun measureSize(text: CharSequence): Pair<Float, Float> {
42
- return measureAndCacheSize(text, cachedYogaWidth)
43
- }
44
-
45
- private fun measureAndCacheSize(text: CharSequence, maxWidth: Float): Pair<Float, Float> {
46
- val result = measureSize(text, maxWidth)
47
- cachedSize = result
48
- return result
49
- }
50
-
51
- private fun measureSize(text: CharSequence, maxWidth: Float): Pair<Float, Float> {
52
- val paint = view.paint
53
- val textLength = text.length
54
-
55
- val builder = StaticLayout.Builder
56
- .obtain(text, 0, textLength, paint, maxWidth.toInt())
57
- .setIncludePad(true)
58
- .setLineSpacing(0f, 1f)
59
-
60
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
61
- builder.setBreakStrategy(LineBreaker.BREAK_STRATEGY_HIGH_QUALITY)
62
- }
63
-
64
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
65
- builder.setUseLineSpacingFromFallbacks(true)
66
- }
67
-
68
- val staticLayout = builder.build()
69
- val heightInSP = PixelUtil.toDIPFromPixel(staticLayout.height.toFloat())
70
- val widthInSP = PixelUtil.toDIPFromPixel(maxWidth)
71
-
72
- return Pair(widthInSP, heightInSP)
22
+ fun releaseMeasurementStore() {
23
+ MeasurementStore.release(view.id)
73
24
  }
74
25
  }
@@ -15,7 +15,6 @@ import com.facebook.react.uimanager.annotations.ReactProp
15
15
  import com.facebook.react.viewmanagers.EnrichedTextInputViewManagerDelegate
16
16
  import com.facebook.react.viewmanagers.EnrichedTextInputViewManagerInterface
17
17
  import com.facebook.yoga.YogaMeasureMode
18
- import com.facebook.yoga.YogaMeasureOutput
19
18
  import com.swmansion.enriched.events.OnInputBlurEvent
20
19
  import com.swmansion.enriched.events.OnChangeHtmlEvent
21
20
  import com.swmansion.enriched.events.OnChangeSelectionEvent
@@ -32,12 +31,8 @@ import com.swmansion.enriched.utils.jsonStringToStringMap
32
31
  @ReactModule(name = EnrichedTextInputViewManager.NAME)
33
32
  class EnrichedTextInputViewManager : SimpleViewManager<EnrichedTextInputView>(),
34
33
  EnrichedTextInputViewManagerInterface<EnrichedTextInputView> {
35
- private val mDelegate: ViewManagerDelegate<EnrichedTextInputView>
36
- private var view: EnrichedTextInputView? = null
37
-
38
- init {
39
- mDelegate = EnrichedTextInputViewManagerDelegate(this)
40
- }
34
+ private val mDelegate: ViewManagerDelegate<EnrichedTextInputView> =
35
+ EnrichedTextInputViewManagerDelegate(this)
41
36
 
42
37
  override fun getDelegate(): ViewManagerDelegate<EnrichedTextInputView>? {
43
38
  return mDelegate
@@ -48,10 +43,12 @@ class EnrichedTextInputViewManager : SimpleViewManager<EnrichedTextInputView>(),
48
43
  }
49
44
 
50
45
  public override fun createViewInstance(context: ThemedReactContext): EnrichedTextInputView {
51
- val view = EnrichedTextInputView(context)
52
- this.view = view
46
+ return EnrichedTextInputView(context)
47
+ }
53
48
 
54
- return view
49
+ override fun onDropViewInstance(view: EnrichedTextInputView) {
50
+ super.onDropViewInstance(view)
51
+ view.layoutManager.releaseMeasurementStore()
55
52
  }
56
53
 
57
54
  override fun updateState(
@@ -80,7 +77,7 @@ class EnrichedTextInputViewManager : SimpleViewManager<EnrichedTextInputView>(),
80
77
 
81
78
  @ReactProp(name = "defaultValue")
82
79
  override fun setDefaultValue(view: EnrichedTextInputView?, value: String?) {
83
- view?.setValue(value)
80
+ view?.setDefaultValue(value)
84
81
  }
85
82
 
86
83
  @ReactProp(name = "placeholder")
@@ -157,9 +154,14 @@ class EnrichedTextInputViewManager : SimpleViewManager<EnrichedTextInputView>(),
157
154
  view?.setFontStyle(style)
158
155
  }
159
156
 
157
+ @ReactProp(name = "scrollEnabled")
158
+ override fun setScrollEnabled(view: EnrichedTextInputView, scrollEnabled: Boolean) {
159
+ view.scrollEnabled = scrollEnabled
160
+ }
161
+
160
162
  override fun onAfterUpdateTransaction(view: EnrichedTextInputView) {
161
163
  super.onAfterUpdateTransaction(view)
162
- view.updateTypeface()
164
+ view.afterUpdateTransaction()
163
165
  }
164
166
 
165
167
  override fun setPadding(
@@ -277,13 +279,8 @@ class EnrichedTextInputViewManager : SimpleViewManager<EnrichedTextInputView>(),
277
279
  heightMode: YogaMeasureMode?,
278
280
  attachmentsPositions: FloatArray?
279
281
  ): Long {
280
- val size = this.view?.layoutManager?.getMeasuredSize(width)
281
-
282
- if (size != null) {
283
- return YogaMeasureOutput.make(size.first, size.second)
284
- }
285
-
286
- return YogaMeasureOutput.make(0, 0)
282
+ val id = localData?.getInt("viewTag")
283
+ return MeasurementStore.getMeasureById(context, id, width, props)
287
284
  }
288
285
 
289
286
  companion object {
@@ -0,0 +1,158 @@
1
+ package com.swmansion.enriched
2
+
3
+ import android.content.Context
4
+ import android.graphics.Typeface
5
+ import android.graphics.text.LineBreaker
6
+ import android.os.Build
7
+ import android.text.Spannable
8
+ import android.text.StaticLayout
9
+ import android.text.TextPaint
10
+ import android.util.Log
11
+ import com.facebook.react.bridge.ReadableMap
12
+ import com.facebook.react.uimanager.PixelUtil
13
+ import com.facebook.react.views.text.ReactTypefaceUtils.applyStyles
14
+ import com.facebook.react.views.text.ReactTypefaceUtils.parseFontStyle
15
+ import com.facebook.react.views.text.ReactTypefaceUtils.parseFontWeight
16
+ import com.facebook.yoga.YogaMeasureOutput
17
+ import com.swmansion.enriched.styles.HtmlStyle
18
+ import com.swmansion.enriched.utils.EnrichedParser
19
+ import java.util.concurrent.ConcurrentHashMap
20
+ import kotlin.math.ceil
21
+
22
+ object MeasurementStore {
23
+ data class PaintParams(
24
+ val typeface: Typeface,
25
+ val fontSize: Float,
26
+ )
27
+
28
+ data class MeasurementParams(
29
+ val initialized: Boolean,
30
+
31
+ val cachedWidth: Float,
32
+ val cachedSize: Long,
33
+
34
+ val spannable: CharSequence?,
35
+ val paintParams: PaintParams,
36
+ )
37
+
38
+ private val data = ConcurrentHashMap<Int, MeasurementParams>()
39
+
40
+ fun store(id: Int, spannable: Spannable?, paint: TextPaint): Boolean {
41
+ val cachedWidth = data[id]?.cachedWidth ?: 0f
42
+ val cachedSize = data[id]?.cachedSize ?: 0L
43
+ val initialized = data[id]?.initialized ?: true
44
+
45
+ val size = measure(cachedWidth, spannable, paint)
46
+ val paintParams = PaintParams(paint.typeface, paint.textSize)
47
+
48
+ data[id] = MeasurementParams(initialized, cachedWidth, size, spannable, paintParams)
49
+ return cachedSize != size
50
+ }
51
+
52
+ fun release(id: Int) {
53
+ data.remove(id)
54
+ }
55
+
56
+ private fun measure(maxWidth: Float, spannable: CharSequence?, paintParams: PaintParams): Long {
57
+ val paint = TextPaint().apply {
58
+ typeface = paintParams.typeface
59
+ textSize = paintParams.fontSize
60
+ }
61
+
62
+ return measure(maxWidth, spannable, paint)
63
+ }
64
+
65
+ private fun measure(maxWidth: Float, spannable: CharSequence?, paint: TextPaint): Long {
66
+ val text = spannable ?: ""
67
+ val textLength = text.length
68
+ val builder = StaticLayout.Builder
69
+ .obtain(text, 0, textLength, paint, maxWidth.toInt())
70
+ .setIncludePad(true)
71
+ .setLineSpacing(0f, 1f)
72
+
73
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
74
+ builder.setBreakStrategy(LineBreaker.BREAK_STRATEGY_HIGH_QUALITY)
75
+ }
76
+
77
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
78
+ builder.setUseLineSpacingFromFallbacks(true)
79
+ }
80
+
81
+ val staticLayout = builder.build()
82
+ val heightInSP = PixelUtil.toDIPFromPixel(staticLayout.height.toFloat())
83
+ val widthInSP = PixelUtil.toDIPFromPixel(maxWidth)
84
+ return YogaMeasureOutput.make(widthInSP, heightInSP)
85
+ }
86
+
87
+ // Returns either: Spannable parsed from HTML defaultValue, or plain text defaultValue, or "I" if no defaultValue
88
+ private fun getInitialText(defaultView: EnrichedTextInputView, props: ReadableMap?): CharSequence {
89
+ val defaultValue = props?.getString("defaultValue")
90
+
91
+ // If there is no default value, assume text is one line, "I" is a good approximation of height
92
+ if (defaultValue == null) return "I"
93
+
94
+ val isHtml = defaultValue.startsWith("<html>") && defaultValue.endsWith("</html>")
95
+ if (!isHtml) return defaultValue
96
+
97
+ try {
98
+ val htmlStyle = HtmlStyle(defaultView, props.getMap("htmlStyle"))
99
+ val parsed = EnrichedParser.fromHtml(defaultValue, htmlStyle, null)
100
+ return parsed.trimEnd('\n')
101
+ } catch (e: Exception) {
102
+ Log.w("MeasurementStore", "Error parsing initial HTML text: ${e.message}")
103
+ return defaultValue
104
+ }
105
+ }
106
+
107
+ private fun getInitialFontSize(defaultView: EnrichedTextInputView, props: ReadableMap?): Float {
108
+ val propsFontSize = props?.getDouble("fontSize")?.toFloat()
109
+ if (propsFontSize == null) return defaultView.textSize
110
+
111
+ return ceil(PixelUtil.toPixelFromSP(propsFontSize))
112
+ }
113
+
114
+ // Called when view measurements are not available in the store
115
+ // Most likely first measurement, we can use defaultValue, as no native state is set yet
116
+ private fun initialMeasure(context: Context, id: Int?, width: Float, props: ReadableMap?): Long {
117
+ val defaultView = EnrichedTextInputView(context)
118
+
119
+ val text = getInitialText(defaultView, props)
120
+ val fontSize = getInitialFontSize(defaultView, props)
121
+
122
+ val fontFamily = props?.getString("fontFamily")
123
+ val fontStyle = parseFontStyle(props?.getString("fontStyle"))
124
+ val fontWeight = parseFontWeight(props?.getString("fontWeight"))
125
+
126
+ val typeface = applyStyles(defaultView.typeface, fontStyle, fontWeight, fontFamily, context.assets)
127
+ val paintParams = PaintParams(typeface, fontSize)
128
+ val size = measure(width, text, PaintParams(typeface, fontSize))
129
+
130
+ if (id != null) {
131
+ data[id] = MeasurementParams(true, width, size, text, paintParams)
132
+ }
133
+
134
+ return size
135
+ }
136
+
137
+ fun getMeasureById(context: Context, id: Int?, width: Float, props: ReadableMap?): Long {
138
+ val id = id ?: return initialMeasure(context, id, width, props)
139
+ val value = data[id] ?: return initialMeasure(context, id, width, props)
140
+
141
+ // First measure has to be done using initialMeasure
142
+ // That way it's free of any side effects and async initializations
143
+ if (!value.initialized) return initialMeasure(context, id, width, props)
144
+
145
+ if (width == value.cachedWidth) {
146
+ return value.cachedSize
147
+ }
148
+
149
+ val paint = TextPaint().apply {
150
+ typeface = value.paintParams.typeface
151
+ textSize = value.paintParams.fontSize
152
+ }
153
+
154
+ val size = measure(width, value.spannable, paint)
155
+ data[id] = MeasurementParams(true, width, size, value.spannable, value.paintParams)
156
+ return size
157
+ }
158
+ }
@@ -1,5 +1,7 @@
1
1
  package com.swmansion.enriched.spans
2
2
 
3
+ import com.swmansion.enriched.styles.HtmlStyle
4
+
3
5
  data class BaseSpanConfig(val clazz: Class<*>)
4
6
  data class ParagraphSpanConfig(val clazz: Class<*>, val isContinuous: Boolean)
5
7
  data class ListSpanConfig(val clazz: Class<*>, val shortcut: String)
@@ -62,50 +64,63 @@ object EnrichedSpans {
62
64
  MENTION to BaseSpanConfig(EnrichedMentionSpan::class.java),
63
65
  )
64
66
 
65
- val mergingConfig: Map<String, StylesMergingConfig> = mapOf(
66
- BOLD to StylesMergingConfig(
67
- blockingStyles = arrayOf(CODE_BLOCK)
68
- ),
69
- ITALIC to StylesMergingConfig(
70
- blockingStyles = arrayOf(CODE_BLOCK)
71
- ),
72
- UNDERLINE to StylesMergingConfig(
73
- blockingStyles = arrayOf(CODE_BLOCK)
74
- ),
75
- STRIKETHROUGH to StylesMergingConfig(
76
- blockingStyles = arrayOf(CODE_BLOCK)
77
- ),
78
- INLINE_CODE to StylesMergingConfig(
79
- conflictingStyles = arrayOf(MENTION, LINK),
80
- blockingStyles = arrayOf(CODE_BLOCK)
81
- ),
82
- H1 to StylesMergingConfig(
83
- conflictingStyles = arrayOf(H2, H3, ORDERED_LIST, UNORDERED_LIST, BLOCK_QUOTE, CODE_BLOCK),
84
- ),
85
- H2 to StylesMergingConfig(
86
- conflictingStyles = arrayOf(H1, H3, ORDERED_LIST, UNORDERED_LIST, BLOCK_QUOTE, CODE_BLOCK),
87
- ),
88
- H3 to StylesMergingConfig(
89
- conflictingStyles = arrayOf(H1, H2, ORDERED_LIST, UNORDERED_LIST, BLOCK_QUOTE, CODE_BLOCK),
90
- ),
91
- BLOCK_QUOTE to StylesMergingConfig(
92
- conflictingStyles = arrayOf(H1, H2, H3, CODE_BLOCK, ORDERED_LIST, UNORDERED_LIST),
93
- ),
94
- CODE_BLOCK to StylesMergingConfig(
95
- conflictingStyles = arrayOf(H1, H2, H3, BOLD, ITALIC, UNDERLINE, STRIKETHROUGH, UNORDERED_LIST, ORDERED_LIST, BLOCK_QUOTE, INLINE_CODE),
96
- ),
97
- UNORDERED_LIST to StylesMergingConfig(
98
- conflictingStyles = arrayOf(H1, H2, H3, ORDERED_LIST, CODE_BLOCK, BLOCK_QUOTE),
99
- ),
100
- ORDERED_LIST to StylesMergingConfig(
101
- conflictingStyles = arrayOf(H1, H2, H3, UNORDERED_LIST, CODE_BLOCK, BLOCK_QUOTE),
102
- ),
103
- LINK to StylesMergingConfig(
104
- blockingStyles = arrayOf(INLINE_CODE, CODE_BLOCK, MENTION)
105
- ),
106
- IMAGE to StylesMergingConfig(),
107
- MENTION to StylesMergingConfig(
108
- blockingStyles = arrayOf(INLINE_CODE, CODE_BLOCK, LINK)
109
- ),
110
- )
67
+ fun getMergingConfigForStyle(style: String, htmlStyle: HtmlStyle): StylesMergingConfig? {
68
+ return when (style) {
69
+ BOLD -> {
70
+ val blockingStyles = mutableListOf(CODE_BLOCK)
71
+ if (htmlStyle.h1Bold) blockingStyles.add(H1)
72
+ if (htmlStyle.h2Bold) blockingStyles.add(H2)
73
+ if (htmlStyle.h3Bold) blockingStyles.add(H3)
74
+ StylesMergingConfig(blockingStyles = blockingStyles.toTypedArray())
75
+ }
76
+ ITALIC -> StylesMergingConfig(
77
+ blockingStyles = arrayOf(CODE_BLOCK)
78
+ )
79
+ UNDERLINE -> StylesMergingConfig(
80
+ blockingStyles = arrayOf(CODE_BLOCK)
81
+ )
82
+ STRIKETHROUGH -> StylesMergingConfig(
83
+ blockingStyles = arrayOf(CODE_BLOCK)
84
+ )
85
+ INLINE_CODE -> StylesMergingConfig(
86
+ conflictingStyles = arrayOf(MENTION, LINK),
87
+ blockingStyles = arrayOf(CODE_BLOCK)
88
+ )
89
+ H1 -> {
90
+ val conflictingStyles = mutableListOf(H2, H3, ORDERED_LIST, UNORDERED_LIST, BLOCK_QUOTE, CODE_BLOCK)
91
+ if (htmlStyle.h1Bold) conflictingStyles.add(BOLD)
92
+ StylesMergingConfig(conflictingStyles = conflictingStyles.toTypedArray())
93
+ }
94
+ H2 -> {
95
+ val conflictingStyles = mutableListOf(H1, H3, ORDERED_LIST, UNORDERED_LIST, BLOCK_QUOTE, CODE_BLOCK)
96
+ if (htmlStyle.h2Bold) conflictingStyles.add(BOLD)
97
+ StylesMergingConfig(conflictingStyles = conflictingStyles.toTypedArray())
98
+ }
99
+ H3 -> {
100
+ val conflictingStyles = mutableListOf(H1, H2, ORDERED_LIST, UNORDERED_LIST, BLOCK_QUOTE, CODE_BLOCK)
101
+ if (htmlStyle.h3Bold) conflictingStyles.add(BOLD)
102
+ StylesMergingConfig(conflictingStyles = conflictingStyles.toTypedArray())
103
+ }
104
+ BLOCK_QUOTE -> StylesMergingConfig(
105
+ conflictingStyles = arrayOf(H1, H2, H3, CODE_BLOCK, ORDERED_LIST, UNORDERED_LIST)
106
+ )
107
+ CODE_BLOCK -> StylesMergingConfig(
108
+ conflictingStyles = arrayOf(H1, H2, H3, BOLD, ITALIC, UNDERLINE, STRIKETHROUGH, UNORDERED_LIST, ORDERED_LIST, BLOCK_QUOTE, INLINE_CODE)
109
+ )
110
+ UNORDERED_LIST -> StylesMergingConfig(
111
+ conflictingStyles = arrayOf(H1, H2, H3, ORDERED_LIST, CODE_BLOCK, BLOCK_QUOTE)
112
+ )
113
+ ORDERED_LIST -> StylesMergingConfig(
114
+ conflictingStyles = arrayOf(H1, H2, H3, UNORDERED_LIST, CODE_BLOCK, BLOCK_QUOTE),
115
+ )
116
+ LINK -> StylesMergingConfig(
117
+ blockingStyles = arrayOf(INLINE_CODE, CODE_BLOCK, MENTION)
118
+ )
119
+ IMAGE -> StylesMergingConfig()
120
+ MENTION -> StylesMergingConfig(
121
+ blockingStyles = arrayOf(INLINE_CODE, CODE_BLOCK, LINK)
122
+ )
123
+ else -> null
124
+ }
125
+ }
111
126
  }
@@ -104,15 +104,31 @@ class ParametrizedStyles(private val view: EnrichedTextInputView) {
104
104
  return Triple(result, start, end)
105
105
  }
106
106
 
107
+ private fun canLinkBeApplied(): Boolean {
108
+ val mergingConfig = EnrichedSpans.getMergingConfigForStyle(EnrichedSpans.LINK, view.htmlStyle)?: return true
109
+ val conflictingStyles = mergingConfig.conflictingStyles
110
+ val blockingStyles = mergingConfig.blockingStyles
111
+
112
+ for (style in blockingStyles) {
113
+ if (view.spanState?.getStart(style) != null) return false
114
+ }
115
+
116
+ for (style in conflictingStyles) {
117
+ if (view.spanState?.getStart(style) != null) return false
118
+ }
119
+
120
+ return true
121
+ }
122
+
107
123
  private fun afterTextChangedLinks(result: Triple<String, Int, Int>) {
108
124
  // Do not detect link if it's applied manually
109
- if (isSettingLinkSpan) return
125
+ if (isSettingLinkSpan || !canLinkBeApplied()) return
126
+
110
127
  val spannable = view.text as Spannable
111
128
  val (word, start, end) = result
112
129
 
113
130
  // TODO: Consider using more reliable regex, this one matches almost anything
114
131
  val urlPattern = android.util.Patterns.WEB_URL.matcher(word)
115
-
116
132
  val spans = spannable.getSpans(start, end, EnrichedLinkSpan::class.java)
117
133
  for (span in spans) {
118
134
  spannable.removeSpan(span)