react-native-enriched 0.1.4 → 0.1.5

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.
@@ -7,6 +7,7 @@ import android.graphics.BlendMode
7
7
  import android.graphics.BlendModeColorFilter
8
8
  import android.graphics.Color
9
9
  import android.graphics.Rect
10
+ import android.graphics.text.LineBreaker
10
11
  import android.os.Build
11
12
  import android.text.InputType
12
13
  import android.text.Spannable
@@ -51,9 +52,6 @@ class EnrichedTextInputView : AppCompatEditText {
51
52
  val paragraphStyles: ParagraphStyles? = ParagraphStyles(this)
52
53
  val listStyles: ListStyles? = ListStyles(this)
53
54
  val parametrizedStyles: ParametrizedStyles? = ParametrizedStyles(this)
54
- // Sometimes setting up style triggers many changes in sequence
55
- // Eg. removing conflicting styles -> changing text -> applying spans
56
- // In such scenario we want to prevent from handling side effects (eg. onTextChanged)
57
55
  var isDuringTransaction: Boolean = false
58
56
  var isRemovingMany: Boolean = false
59
57
 
@@ -102,6 +100,10 @@ class EnrichedTextInputView : AppCompatEditText {
102
100
  gravity = Gravity.TOP or Gravity.START
103
101
  inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_MULTI_LINE
104
102
 
103
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
104
+ breakStrategy = LineBreaker.BREAK_STRATEGY_HIGH_QUALITY
105
+ }
106
+
105
107
  setPadding(0, 0, 0, 0)
106
108
  setBackgroundColor(Color.TRANSPARENT)
107
109
 
@@ -236,18 +238,17 @@ class EnrichedTextInputView : AppCompatEditText {
236
238
 
237
239
  fun setValue(value: CharSequence?) {
238
240
  if (value == null) return
239
- isDuringTransaction = true
240
241
 
241
- val newText = parseText(value)
242
- setText(newText)
242
+ runAsATransaction {
243
+ val newText = parseText(value)
244
+ setText(newText)
243
245
 
244
- // Assign SpanWatcher one more time as our previous spannable has been replaced
245
- addSpanWatcher(EnrichedSpanWatcher(this))
246
-
247
- // Scroll to the last line of text
248
- setSelection(text?.length ?: 0)
246
+ // Assign SpanWatcher one more time as our previous spannable has been replaced
247
+ addSpanWatcher(EnrichedSpanWatcher(this))
249
248
 
250
- isDuringTransaction = false
249
+ // Scroll to the last line of text
250
+ setSelection(text?.length ?: 0)
251
+ }
251
252
  }
252
253
 
253
254
  fun setAutoFocus(autoFocus: Boolean) {
@@ -455,13 +456,13 @@ class EnrichedTextInputView : AppCompatEditText {
455
456
  val end = selection?.end ?: 0
456
457
  val lengthBefore = text?.length ?: 0
457
458
 
458
- isDuringTransaction = true
459
- val targetRange = getTargetRange(name)
460
- val removed = removeStyle(style, targetRange.first, targetRange.second)
461
- if (removed) {
462
- spanState?.setStart(style, null)
459
+ runAsATransaction {
460
+ val targetRange = getTargetRange(name)
461
+ val removed = removeStyle(style, targetRange.first, targetRange.second)
462
+ if (removed) {
463
+ spanState?.setStart(style, null)
464
+ }
463
465
  }
464
- isDuringTransaction = false
465
466
 
466
467
  val lengthAfter = text?.length ?: 0
467
468
  val charactersRemoved = lengthBefore - lengthAfter
@@ -519,6 +520,18 @@ class EnrichedTextInputView : AppCompatEditText {
519
520
  parametrizedStyles?.setMentionSpan(text, indicator, attributes)
520
521
  }
521
522
 
523
+ // Sometimes setting up style triggers many changes in sequence
524
+ // Eg. removing conflicting styles -> changing text -> applying spans
525
+ // In such scenario we want to prevent from handling side effects (eg. onTextChanged)
526
+ fun runAsATransaction(block: () -> Unit) {
527
+ try {
528
+ isDuringTransaction = true
529
+ block()
530
+ } finally {
531
+ isDuringTransaction = false
532
+ }
533
+ }
534
+
522
535
  override fun onAttachedToWindow() {
523
536
  super.onAttachedToWindow()
524
537
 
@@ -1,5 +1,7 @@
1
1
  package com.swmansion.enriched
2
2
 
3
+ import android.graphics.text.LineBreaker
4
+ import android.os.Build
3
5
  import android.text.Editable
4
6
  import android.text.StaticLayout
5
7
  import com.facebook.react.bridge.Arguments
@@ -50,12 +52,20 @@ class EnrichedTextInputViewLayoutManager(private val view: EnrichedTextInputView
50
52
  val paint = view.paint
51
53
  val textLength = text.length
52
54
 
53
- val staticLayout = StaticLayout.Builder
55
+ val builder = StaticLayout.Builder
54
56
  .obtain(text, 0, textLength, paint, maxWidth.toInt())
55
57
  .setIncludePad(true)
56
58
  .setLineSpacing(0f, 1f)
57
- .build()
58
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()
59
69
  val heightInSP = PixelUtil.toDIPFromPixel(staticLayout.height.toFloat())
60
70
  val widthInSP = PixelUtil.toDIPFromPixel(maxWidth)
61
71
 
@@ -4,13 +4,17 @@ import android.graphics.Canvas
4
4
  import android.graphics.Paint
5
5
  import android.text.Layout
6
6
  import android.text.TextPaint
7
- import android.text.style.CharacterStyle
8
7
  import android.text.style.LeadingMarginSpan
8
+ import android.text.style.MetricAffectingSpan
9
9
  import com.swmansion.enriched.spans.interfaces.EnrichedBlockSpan
10
10
  import com.swmansion.enriched.styles.HtmlStyle
11
11
 
12
12
  // https://android.googlesource.com/platform/frameworks/base/+/refs/heads/main/core/java/android/text/style/QuoteSpan.java
13
- class EnrichedBlockQuoteSpan(private val htmlStyle: HtmlStyle) : CharacterStyle(), LeadingMarginSpan, EnrichedBlockSpan {
13
+ class EnrichedBlockQuoteSpan(private val htmlStyle: HtmlStyle) : MetricAffectingSpan(), LeadingMarginSpan, EnrichedBlockSpan {
14
+ override fun updateMeasureState(p0: TextPaint) {
15
+ // Do nothing, but inform layout that this span affects text metrics
16
+ }
17
+
14
18
  override fun getLeadingMargin(p0: Boolean): Int {
15
19
  return htmlStyle.blockquoteStripeWidth + htmlStyle.blockquoteGapWidth
16
20
  }
@@ -5,15 +5,19 @@ import android.graphics.Paint
5
5
  import android.graphics.RectF
6
6
  import android.graphics.Typeface
7
7
  import android.text.TextPaint
8
- import android.text.style.CharacterStyle
9
8
  import android.text.style.LineBackgroundSpan
9
+ import android.text.style.MetricAffectingSpan
10
10
  import com.swmansion.enriched.spans.interfaces.EnrichedBlockSpan
11
11
  import com.swmansion.enriched.styles.HtmlStyle
12
12
 
13
- class EnrichedCodeBlockSpan(private val htmlStyle: HtmlStyle) : CharacterStyle(), LineBackgroundSpan, EnrichedBlockSpan {
14
- override fun updateDrawState(paint: TextPaint?) {
15
- paint?.typeface = Typeface.MONOSPACE
16
- paint?.color = htmlStyle.codeBlockColor
13
+ class EnrichedCodeBlockSpan(private val htmlStyle: HtmlStyle) : MetricAffectingSpan(), LineBackgroundSpan, EnrichedBlockSpan {
14
+ override fun updateDrawState(paint: TextPaint) {
15
+ paint.typeface = Typeface.MONOSPACE
16
+ paint.color = htmlStyle.codeBlockColor
17
+ }
18
+
19
+ override fun updateMeasureState(paint: TextPaint) {
20
+ paint.typeface = Typeface.MONOSPACE
17
21
  }
18
22
 
19
23
  override fun drawBackground(
@@ -2,15 +2,20 @@ package com.swmansion.enriched.spans
2
2
 
3
3
  import android.graphics.Typeface
4
4
  import android.text.TextPaint
5
- import android.text.style.BackgroundColorSpan
5
+ import android.text.style.MetricAffectingSpan
6
6
  import com.swmansion.enriched.spans.interfaces.EnrichedInlineSpan
7
7
  import com.swmansion.enriched.styles.HtmlStyle
8
8
 
9
- class EnrichedInlineCodeSpan(private val htmlStyle: HtmlStyle) : BackgroundColorSpan(htmlStyle.inlineCodeBackgroundColor), EnrichedInlineSpan {
9
+ class EnrichedInlineCodeSpan(private val htmlStyle: HtmlStyle) : MetricAffectingSpan(), EnrichedInlineSpan {
10
10
  override fun updateDrawState(textPaint: TextPaint) {
11
- super.updateDrawState(textPaint)
12
-
11
+ val typeface = Typeface.create(Typeface.MONOSPACE, Typeface.NORMAL)
12
+ textPaint.typeface = typeface
13
13
  textPaint.color = htmlStyle.inlineCodeColor
14
- textPaint.typeface = Typeface.create(Typeface.MONOSPACE, Typeface.NORMAL)
14
+ textPaint.bgColor = htmlStyle.inlineCodeBackgroundColor
15
+ }
16
+
17
+ override fun updateMeasureState(textPaint: TextPaint) {
18
+ val typeface = Typeface.create(Typeface.MONOSPACE, Typeface.NORMAL)
19
+ textPaint.typeface = typeface
15
20
  }
16
21
  }
@@ -4,11 +4,21 @@ import android.graphics.Canvas
4
4
  import android.graphics.Paint
5
5
  import android.graphics.Typeface
6
6
  import android.text.Layout
7
+ import android.text.TextPaint
7
8
  import android.text.style.LeadingMarginSpan
9
+ import android.text.style.MetricAffectingSpan
8
10
  import com.swmansion.enriched.spans.interfaces.EnrichedParagraphSpan
9
11
  import com.swmansion.enriched.styles.HtmlStyle
10
12
 
11
- class EnrichedOrderedListSpan(private var index: Int, private val htmlStyle: HtmlStyle) : LeadingMarginSpan, EnrichedParagraphSpan {
13
+ class EnrichedOrderedListSpan(private var index: Int, private val htmlStyle: HtmlStyle) : MetricAffectingSpan(), LeadingMarginSpan, EnrichedParagraphSpan {
14
+ override fun updateMeasureState(p0: TextPaint) {
15
+ // Do nothing, but inform layout that this span affects text metrics
16
+ }
17
+
18
+ override fun updateDrawState(p0: TextPaint?) {
19
+ // Do nothing, but inform layout that this span affects text metrics
20
+ }
21
+
12
22
  override fun getLeadingMargin(first: Boolean): Int {
13
23
  return htmlStyle.olMarginLeft + htmlStyle.olGapWidth
14
24
  }
@@ -4,12 +4,22 @@ import android.graphics.Canvas
4
4
  import android.graphics.Paint
5
5
  import android.text.Layout
6
6
  import android.text.Spanned
7
+ import android.text.TextPaint
7
8
  import android.text.style.LeadingMarginSpan
9
+ import android.text.style.MetricAffectingSpan
8
10
  import com.swmansion.enriched.spans.interfaces.EnrichedParagraphSpan
9
11
  import com.swmansion.enriched.styles.HtmlStyle
10
12
 
11
13
  // https://android.googlesource.com/platform/frameworks/base/+/refs/heads/main/core/java/android/text/style/BulletSpan.java
12
- class EnrichedUnorderedListSpan(private val htmlStyle: HtmlStyle) : LeadingMarginSpan, EnrichedParagraphSpan {
14
+ class EnrichedUnorderedListSpan(private val htmlStyle: HtmlStyle) : MetricAffectingSpan(), LeadingMarginSpan, EnrichedParagraphSpan {
15
+ override fun updateMeasureState(p0: TextPaint) {
16
+ // Do nothing, but inform layout that this span affects text metrics
17
+ }
18
+
19
+ override fun updateDrawState(p0: TextPaint?) {
20
+ // Do nothing, but inform layout that this span affects text metrics
21
+ }
22
+
13
23
  override fun getLeadingMargin(p0: Boolean): Int {
14
24
  return htmlStyle.ulBulletSize + htmlStyle.ulGapWidth + htmlStyle.ulMarginLeft
15
25
  }
@@ -58,12 +58,11 @@ class ListStyles(private val view: EnrichedTextInputView) {
58
58
  val spans = ssb.getSpans(start, end, clazz)
59
59
  if (spans.isEmpty()) return false
60
60
 
61
- ssb.replace(start, end, ssb.substring(start, end).replace("\u200B", ""))
62
-
63
61
  for (span in spans) {
64
62
  ssb.removeSpan(span)
65
63
  }
66
64
 
65
+ ssb.replace(start, end, ssb.substring(start, end).replace("\u200B", ""))
67
66
  return true
68
67
  }
69
68
 
@@ -201,20 +201,21 @@ class ParametrizedStyles(private val view: EnrichedTextInputView) {
201
201
  }
202
202
 
203
203
  val start = mentionStart ?: return
204
- view.isDuringTransaction = true
205
- spannable.replace(start, selectionEnd, text)
206
204
 
207
- val span = EnrichedMentionSpan(text, indicator, attributes, view.htmlStyle)
208
- val spanEnd = start + text.length
209
- val (safeStart, safeEnd) = spannable.getSafeSpanBoundaries(start, spanEnd)
210
- spannable.setSpan(span, safeStart, safeEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
205
+ view.runAsATransaction {
206
+ spannable.replace(start, selectionEnd, text)
211
207
 
212
- val hasSpaceAtTheEnd = spannable.length > safeEnd && spannable[safeEnd] == ' '
213
- if (!hasSpaceAtTheEnd) {
214
- spannable.insert(safeEnd, " ")
208
+ val span = EnrichedMentionSpan(text, indicator, attributes, view.htmlStyle)
209
+ val spanEnd = start + text.length
210
+ val (safeStart, safeEnd) = spannable.getSafeSpanBoundaries(start, spanEnd)
211
+ spannable.setSpan(span, safeStart, safeEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
212
+
213
+ val hasSpaceAtTheEnd = spannable.length > safeEnd && spannable[safeEnd] == ' '
214
+ if (!hasSpaceAtTheEnd) {
215
+ spannable.insert(safeEnd, " ")
216
+ }
215
217
  }
216
218
 
217
- view.isDuringTransaction = false
218
219
  view.mentionHandler?.reset()
219
220
  view.selection.validateStyles()
220
221
  }
@@ -1091,10 +1091,15 @@ Class<RCTComponentViewProtocol> EnrichedTextInputViewCls(void) {
1091
1091
  [self tryUpdatingHeight];
1092
1092
  // update active styles as well
1093
1093
  [self tryUpdatingActiveStyles];
1094
+
1094
1095
  // update drawing
1095
- NSRange wholeRange = NSMakeRange(0, textView.textStorage.string.length);
1096
- [textView.layoutManager invalidateLayoutForCharacterRange:wholeRange actualCharacterRange:nullptr];
1097
- [textView.layoutManager invalidateDisplayForCharacterRange:wholeRange];
1096
+ dispatch_async(dispatch_get_main_queue(), ^{
1097
+ NSRange wholeRange = NSMakeRange(0, textView.textStorage.string.length);
1098
+ NSRange actualRange = NSMakeRange(0, 0);
1099
+ [textView.layoutManager invalidateLayoutForCharacterRange:wholeRange actualCharacterRange:&actualRange];
1100
+ [textView.layoutManager ensureLayoutForCharacterRange:actualRange];
1101
+ [textView.layoutManager invalidateDisplayForCharacterRange:wholeRange];
1102
+ });
1098
1103
  }
1099
1104
 
1100
1105
  // MARK: - UITextView delegate methods
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-enriched",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Rich Text Editor component for React Native",
5
5
  "source": "./src/index.tsx",
6
6
  "main": "./lib/module/index.js",
@@ -39,7 +39,8 @@
39
39
  "clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib",
40
40
  "prepare": "bob build",
41
41
  "release": "release-it",
42
- "android-studio": "open -a 'Android Studio' example/android"
42
+ "android-studio": "open -a 'Android Studio' example/android",
43
+ "xcode": "open -a 'Xcode' example/ios/EnrichedTextInputExample.xcworkspace"
43
44
  },
44
45
  "keywords": [
45
46
  "react-native",