react-native-enriched 0.1.3 → 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.
Files changed (34) hide show
  1. package/README.md +36 -648
  2. package/ReactNativeEnriched.podspec +1 -1
  3. package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputView.kt +32 -16
  4. package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewLayoutManager.kt +12 -2
  5. package/android/src/main/java/com/swmansion/enriched/events/MentionHandler.kt +5 -0
  6. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedBlockQuoteSpan.kt +6 -2
  7. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedCodeBlockSpan.kt +9 -5
  8. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedInlineCodeSpan.kt +10 -5
  9. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedOrderedListSpan.kt +11 -1
  10. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedUnorderedListSpan.kt +11 -1
  11. package/android/src/main/java/com/swmansion/enriched/styles/ListStyles.kt +1 -2
  12. package/android/src/main/java/com/swmansion/enriched/styles/ParametrizedStyles.kt +12 -8
  13. package/android/src/main/java/com/swmansion/enriched/utils/EnrichedSelection.kt +1 -1
  14. package/android/src/main/java/com/swmansion/enriched/watchers/EnrichedTextWatcher.kt +2 -2
  15. package/android/src/main/new_arch/CMakeLists.txt +13 -9
  16. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputMeasurementManager.h +2 -2
  17. package/ios/EnrichedTextInputView.mm +64 -56
  18. package/ios/internals/EnrichedTextInputViewShadowNode.mm +4 -1
  19. package/ios/styles/BlockQuoteStyle.mm +18 -23
  20. package/ios/styles/BoldStyle.mm +5 -2
  21. package/ios/styles/HeadingStyleBase.mm +37 -5
  22. package/ios/styles/InlineCodeStyle.mm +5 -2
  23. package/ios/styles/ItalicStyle.mm +5 -5
  24. package/ios/styles/OrderedListStyle.mm +18 -23
  25. package/ios/styles/StrikethroughStyle.mm +5 -2
  26. package/ios/styles/UnderlineStyle.mm +5 -2
  27. package/ios/styles/UnorderedListStyle.mm +18 -23
  28. package/ios/utils/OccurenceUtils.h +6 -0
  29. package/ios/utils/OccurenceUtils.mm +31 -0
  30. package/ios/utils/ParagraphAttributesUtils.h +6 -0
  31. package/ios/utils/ParagraphAttributesUtils.mm +67 -0
  32. package/ios/utils/StyleHeaders.h +2 -0
  33. package/ios/utils/ZeroWidthSpaceUtils.mm +14 -6
  34. package/package.json +6 -5
@@ -11,7 +11,7 @@ Pod::Spec.new do |s|
11
11
  s.authors = package["author"]
12
12
 
13
13
  s.platforms = { :ios => min_ios_version_supported }
14
- s.source = { :git => "https://github.com/software-mansion-labs/react-native-rich-text-editor.git", :tag => "#{s.version}" }
14
+ s.source = { :git => "https://github.com/software-mansion/react-native-enriched.git", :tag => "#{s.version}" }
15
15
 
16
16
  s.source_files = "ios/**/*.{h,m,mm,cpp}"
17
17
  s.private_header_files = "ios/**/*.h"
@@ -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,7 +52,7 @@ 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
- var isSettingValue: Boolean = false
55
+ var isDuringTransaction: Boolean = false
55
56
  var isRemovingMany: Boolean = false
56
57
 
57
58
  val mentionHandler: MentionHandler? = MentionHandler(this)
@@ -99,6 +100,10 @@ class EnrichedTextInputView : AppCompatEditText {
99
100
  gravity = Gravity.TOP or Gravity.START
100
101
  inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_MULTI_LINE
101
102
 
103
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
104
+ breakStrategy = LineBreaker.BREAK_STRATEGY_HIGH_QUALITY
105
+ }
106
+
102
107
  setPadding(0, 0, 0, 0)
103
108
  setBackgroundColor(Color.TRANSPARENT)
104
109
 
@@ -233,18 +238,17 @@ class EnrichedTextInputView : AppCompatEditText {
233
238
 
234
239
  fun setValue(value: CharSequence?) {
235
240
  if (value == null) return
236
- isSettingValue = true
237
241
 
238
- val newText = parseText(value)
239
- setText(newText)
242
+ runAsATransaction {
243
+ val newText = parseText(value)
244
+ setText(newText)
240
245
 
241
- // Assign SpanWatcher one more time as our previous spannable has been replaced
242
- addSpanWatcher(EnrichedSpanWatcher(this))
243
-
244
- // Scroll to the last line of text
245
- setSelection(text?.length ?: 0)
246
+ // Assign SpanWatcher one more time as our previous spannable has been replaced
247
+ addSpanWatcher(EnrichedSpanWatcher(this))
246
248
 
247
- isSettingValue = false
249
+ // Scroll to the last line of text
250
+ setSelection(text?.length ?: 0)
251
+ }
248
252
  }
249
253
 
250
254
  fun setAutoFocus(autoFocus: Boolean) {
@@ -452,13 +456,13 @@ class EnrichedTextInputView : AppCompatEditText {
452
456
  val end = selection?.end ?: 0
453
457
  val lengthBefore = text?.length ?: 0
454
458
 
455
- isSettingValue = true
456
- val targetRange = getTargetRange(name)
457
- val removed = removeStyle(style, targetRange.first, targetRange.second)
458
- if (removed) {
459
- 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
+ }
460
465
  }
461
- isSettingValue = false
462
466
 
463
467
  val lengthAfter = text?.length ?: 0
464
468
  val charactersRemoved = lengthBefore - lengthAfter
@@ -516,6 +520,18 @@ class EnrichedTextInputView : AppCompatEditText {
516
520
  parametrizedStyles?.setMentionSpan(text, indicator, attributes)
517
521
  }
518
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
+
519
535
  override fun onAttachedToWindow() {
520
536
  super.onAttachedToWindow()
521
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
 
@@ -8,6 +8,11 @@ class MentionHandler(private val view: EnrichedTextInputView) {
8
8
  private var previousText: String? = null
9
9
  private var previousIndicator: String? = null
10
10
 
11
+ fun reset() {
12
+ previousText = null
13
+ previousIndicator = null
14
+ }
15
+
11
16
  fun endMention() {
12
17
  val indicator = previousIndicator
13
18
  if (indicator == null) return
@@ -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,18 +201,22 @@ class ParametrizedStyles(private val view: EnrichedTextInputView) {
201
201
  }
202
202
 
203
203
  val start = mentionStart ?: return
204
- spannable.replace(start, selectionEnd, text)
205
204
 
206
- val span = EnrichedMentionSpan(text, indicator, attributes, view.htmlStyle)
207
- val spanEnd = start + text.length
208
- val (safeStart, safeEnd) = spannable.getSafeSpanBoundaries(start, spanEnd)
209
- spannable.setSpan(span, safeStart, safeEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
205
+ view.runAsATransaction {
206
+ spannable.replace(start, selectionEnd, text)
210
207
 
211
- val hasSpaceAtTheEnd = spannable.length > safeEnd && spannable[safeEnd] == ' '
212
- if (!hasSpaceAtTheEnd) {
213
- 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
+ }
214
217
  }
215
218
 
219
+ view.mentionHandler?.reset()
216
220
  view.selection.validateStyles()
217
221
  }
218
222
 
@@ -39,7 +39,7 @@ class EnrichedSelection(private val view: EnrichedTextInputView) {
39
39
  val finalStart = newStart.coerceAtMost(newEnd).coerceAtLeast(0).coerceAtMost(textLength)
40
40
  val finalEnd = newEnd.coerceAtLeast(newStart).coerceAtLeast(0).coerceAtMost(textLength)
41
41
 
42
- if (isZeroWidthSelection(finalStart, finalEnd) && !view.isSettingValue) {
42
+ if (isZeroWidthSelection(finalStart, finalEnd) && !view.isDuringTransaction) {
43
43
  view.setSelection(finalStart + 1)
44
44
  shouldValidateStyles = false
45
45
  }
@@ -18,14 +18,14 @@ class EnrichedTextWatcher(private val view: EnrichedTextInputView) : TextWatcher
18
18
  override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
19
19
  endCursorPosition = start + count
20
20
  view.layoutManager.measureSize(s ?: "")
21
- view.isRemovingMany = !view.isSettingValue && before > count + 1
21
+ view.isRemovingMany = !view.isDuringTransaction && before > count + 1
22
22
  }
23
23
 
24
24
  override fun afterTextChanged(s: Editable?) {
25
25
  if (s == null) return
26
26
  emitEvents(s)
27
27
 
28
- if (view.isSettingValue) return
28
+ if (view.isDuringTransaction) return
29
29
  applyStyles(s)
30
30
  }
31
31
 
@@ -39,15 +39,19 @@ target_link_libraries(
39
39
  ReactAndroid::reactnative
40
40
  )
41
41
 
42
- target_compile_options(
43
- ${LIB_TARGET_NAME}
44
- PRIVATE
45
- -DLOG_TAG=\"ReactNative\"
46
- -fexceptions
47
- -frtti
48
- -Wall
49
- -std=c++20
50
- )
42
+ if(ReactAndroid_VERSION_MINOR GREATER_EQUAL 80)
43
+ target_compile_reactnative_options(${LIB_TARGET_NAME} PRIVATE)
44
+ else()
45
+ target_compile_options(
46
+ ${LIB_TARGET_NAME}
47
+ PRIVATE
48
+ -DLOG_TAG=\"ReactNative\"
49
+ -fexceptions
50
+ -frtti
51
+ -Wall
52
+ -std=c++20
53
+ )
54
+ endif()
51
55
 
52
56
  target_include_directories(
53
57
  ${CMAKE_PROJECT_NAME}
@@ -11,7 +11,7 @@ namespace facebook::react {
11
11
  class EnrichedTextInputMeasurementManager {
12
12
  public:
13
13
  EnrichedTextInputMeasurementManager(
14
- const ContextContainer::Shared& contextContainer)
14
+ const std::shared_ptr<const ContextContainer>& contextContainer)
15
15
  : contextContainer_(contextContainer) {}
16
16
 
17
17
  Size measure(
@@ -20,7 +20,7 @@ namespace facebook::react {
20
20
  LayoutConstraints layoutConstraints) const;
21
21
 
22
22
  private:
23
- const ContextContainer::Shared contextContainer_;
23
+ const std::shared_ptr<const ContextContainer> contextContainer_;
24
24
  };
25
25
 
26
26
  } // namespace facebook::react
@@ -14,6 +14,7 @@
14
14
  #import "WordsUtils.h"
15
15
  #import "LayoutManagerExtension.h"
16
16
  #import "ZeroWidthSpaceUtils.h"
17
+ #import "ParagraphAttributesUtils.h"
17
18
 
18
19
  using namespace facebook::react;
19
20
 
@@ -422,7 +423,7 @@ Class<RCTComponentViewProtocol> EnrichedTextInputViewCls(void) {
422
423
  }
423
424
 
424
425
  // editable
425
- if(newViewProps.editable != oldViewProps.editable) {
426
+ if(newViewProps.editable != textView.editable) {
426
427
  textView.editable = newViewProps.editable;
427
428
  }
428
429
 
@@ -564,7 +565,7 @@ Class<RCTComponentViewProtocol> EnrichedTextInputViewCls(void) {
564
565
  unichar lastChar = [currentStr.string characterAtIndex:currentStr.length-1];
565
566
  if([[NSCharacterSet newlineCharacterSet] characterIsMember:lastChar]) {
566
567
  [currentStr appendAttributedString:
567
- [[NSAttributedString alloc] initWithString:@"I" attributes:textView.typingAttributes]
568
+ [[NSAttributedString alloc] initWithString:@"I" attributes:defaultTypingAttributes]
568
569
  ];
569
570
  }
570
571
  }
@@ -977,6 +978,22 @@ Class<RCTComponentViewProtocol> EnrichedTextInputViewCls(void) {
977
978
  [mentionStyleClass manageMentionTypingAttributes];
978
979
  [mentionStyleClass manageMentionEditing];
979
980
  }
981
+
982
+ // typing attributes for empty lines selection reset
983
+ NSString *currentString = [textView.textStorage.string copy];
984
+ if(textView.selectedRange.length == 0 && [_recentlyEmittedString isEqualToString:currentString]) {
985
+ // no string change means only a selection changed with no character changes
986
+ NSRange paragraphRange = [textView.textStorage.string paragraphRangeForRange:textView.selectedRange];
987
+ if(
988
+ paragraphRange.length == 0 ||
989
+ (paragraphRange.length == 1 &&
990
+ [[NSCharacterSet newlineCharacterSet] characterIsMember:[textView.textStorage.string characterAtIndex:paragraphRange.location]])
991
+ ) {
992
+ // user changed selection to an empty line (or empty line with a newline)
993
+ // typing attributes need to be reset
994
+ textView.typingAttributes = defaultTypingAttributes;
995
+ }
996
+ }
980
997
  }
981
998
 
982
999
  - (void)handleWordModificationBasedChanges:(NSString*)word inRange:(NSRange)range {
@@ -995,15 +1012,15 @@ Class<RCTComponentViewProtocol> EnrichedTextInputViewCls(void) {
995
1012
  return;
996
1013
  }
997
1014
 
1015
+ // zero width space adding or removal
1016
+ [ZeroWidthSpaceUtils handleZeroWidthSpacesInInput:self];
1017
+
998
1018
  // emptying input typing attributes management
999
1019
  if(textView.textStorage.string.length == 0 && _recentlyEmittedString.length > 0) {
1000
1020
  // reset typing attribtues
1001
1021
  textView.typingAttributes = defaultTypingAttributes;
1002
1022
  }
1003
1023
 
1004
- // zero width space removal
1005
- [ZeroWidthSpaceUtils handleZeroWidthSpacesInInput:self];
1006
-
1007
1024
  // inline code on newlines fix
1008
1025
  InlineCodeStyle *codeStyle = stylesDict[@([InlineCodeStyle getStyleType])];
1009
1026
  if(codeStyle != nullptr) {
@@ -1016,6 +1033,16 @@ Class<RCTComponentViewProtocol> EnrichedTextInputViewCls(void) {
1016
1033
  [bqStyle manageBlockquoteColor];
1017
1034
  }
1018
1035
 
1036
+ // improper headings fix
1037
+ H1Style *h1Style = stylesDict[@([H1Style getStyleType])];
1038
+ H2Style *h2Style = stylesDict[@([H2Style getStyleType])];
1039
+ H3Style *h3Style = stylesDict[@([H3Style getStyleType])];
1040
+ if(h1Style != nullptr && h2Style != nullptr && h3Style != nullptr) {
1041
+ [h1Style handleImproperHeadings];
1042
+ [h2Style handleImproperHeadings];
1043
+ [h3Style handleImproperHeadings];
1044
+ }
1045
+
1019
1046
  // placholder management
1020
1047
  if(!_placeholderLabel.hidden && textView.textStorage.string.length > 0) {
1021
1048
  [self setPlaceholderLabelShown:NO];
@@ -1064,10 +1091,15 @@ Class<RCTComponentViewProtocol> EnrichedTextInputViewCls(void) {
1064
1091
  [self tryUpdatingHeight];
1065
1092
  // update active styles as well
1066
1093
  [self tryUpdatingActiveStyles];
1094
+
1067
1095
  // update drawing
1068
- NSRange wholeRange = NSMakeRange(0, textView.textStorage.string.length);
1069
- [textView.layoutManager invalidateLayoutForCharacterRange:wholeRange actualCharacterRange:nullptr];
1070
- [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
+ });
1071
1103
  }
1072
1104
 
1073
1105
  // MARK: - UITextView delegate methods
@@ -1102,61 +1134,37 @@ Class<RCTComponentViewProtocol> EnrichedTextInputViewCls(void) {
1102
1134
  - (bool)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {
1103
1135
  recentlyChangedRange = NSMakeRange(range.location, text.length);
1104
1136
 
1105
- BOOL rejectTextChanges = NO;
1106
-
1107
1137
  UnorderedListStyle *uStyle = stylesDict[@([UnorderedListStyle getStyleType])];
1108
- if(uStyle != nullptr) {
1109
- // removing first line list fix
1110
- BOOL removedFirstLineList = [uStyle handleBackspaceInRange:range replacementText:text];
1111
-
1112
- // creating unordered list from "- "
1113
- BOOL addedShortcutList = [uStyle tryHandlingListShorcutInRange:range replacementText:text];
1114
-
1115
- // any of these changes make us reject text changes
1116
- rejectTextChanges = rejectTextChanges || removedFirstLineList || addedShortcutList;
1117
- }
1118
-
1119
1138
  OrderedListStyle *oStyle = stylesDict[@([OrderedListStyle getStyleType])];
1120
- if(oStyle != nullptr) {
1121
- // removing first line list fix
1122
- BOOL removedFirstLineList = [oStyle handleBackspaceInRange:range replacementText:text];
1123
-
1124
- // creating ordered list from "1."
1125
- BOOL addedShortcutList = [oStyle tryHandlingListShorcutInRange:range replacementText:text];
1126
-
1127
- // any of these changes make us reject text changes
1128
- rejectTextChanges = rejectTextChanges || removedFirstLineList || addedShortcutList;
1129
- }
1130
-
1131
1139
  BlockQuoteStyle *bqStyle = stylesDict[@([BlockQuoteStyle getStyleType])];
1132
- if(bqStyle != nullptr) {
1133
- // removing first line quote fix
1134
- BOOL removedFirstLineQuote = [bqStyle handleBackspaceInRange:range replacementText:text];
1135
- rejectTextChanges = rejectTextChanges || removedFirstLineQuote;
1136
- }
1137
-
1138
1140
  LinkStyle *linkStyle = stylesDict[@([LinkStyle getStyleType])];
1139
- if(linkStyle != nullptr) {
1140
- // persisting link attributes fix
1141
- BOOL fixedLeadingAttributes = [linkStyle handleLeadingLinkReplacement:range replacementText:text];
1142
- rejectTextChanges = rejectTextChanges || fixedLeadingAttributes;
1143
- }
1144
-
1145
1141
  MentionStyle *mentionStyle = stylesDict[@([MentionStyle getStyleType])];
1146
- if(mentionStyle != nullptr) {
1147
- // persisting mention attributes fix
1148
- BOOL fixedLeadingAttributes = [mentionStyle handleLeadingMentionReplacement:range replacementText:text];
1149
- rejectTextChanges = rejectTextChanges || fixedLeadingAttributes;
1150
- }
1151
-
1152
- // zero width space removal fix
1153
- rejectTextChanges = rejectTextChanges || [ZeroWidthSpaceUtils handleBackspaceInRange:range replacementText:text input:self];
1154
-
1155
- if(rejectTextChanges) {
1142
+ H1Style *h1Style = stylesDict[@([H1Style getStyleType])];
1143
+ H2Style *h2Style = stylesDict[@([H2Style getStyleType])];
1144
+ H3Style *h3Style = stylesDict[@([H3Style getStyleType])];
1145
+
1146
+ // some of the changes these checks do could interfere with later checks and cause a crash
1147
+ // so here I rely on short circuiting evaluation of the logical expression
1148
+ // either way it's not possible to have two of them come off at the same time
1149
+ if(
1150
+ [uStyle handleBackspaceInRange:range replacementText:text] ||
1151
+ [uStyle tryHandlingListShorcutInRange:range replacementText:text] ||
1152
+ [oStyle handleBackspaceInRange:range replacementText:text] ||
1153
+ [oStyle tryHandlingListShorcutInRange:range replacementText:text] ||
1154
+ [bqStyle handleBackspaceInRange:range replacementText:text] ||
1155
+ [linkStyle handleLeadingLinkReplacement:range replacementText:text] ||
1156
+ [mentionStyle handleLeadingMentionReplacement:range replacementText:text] ||
1157
+ [h1Style handleNewlinesInRange:range replacementText:text] ||
1158
+ [h2Style handleNewlinesInRange:range replacementText:text] ||
1159
+ [h3Style handleNewlinesInRange:range replacementText:text] ||
1160
+ [ZeroWidthSpaceUtils handleBackspaceInRange:range replacementText:text input:self] ||
1161
+ [ParagraphAttributesUtils handleBackspaceInRange:range replacementText:text input:self]
1162
+ ) {
1156
1163
  [self anyTextMayHaveBeenModified];
1164
+ return NO;
1157
1165
  }
1158
-
1159
- return !rejectTextChanges;
1166
+
1167
+ return YES;
1160
1168
  }
1161
1169
 
1162
1170
  - (void)textViewDidChangeSelection:(UITextView *)textView {
@@ -55,7 +55,10 @@ Size EnrichedTextInputViewShadowNode::measureContent(const LayoutContext& layout
55
55
  });
56
56
  }
57
57
 
58
- return {estimatedSize.width, estimatedSize.height};
58
+ return {
59
+ estimatedSize.width,
60
+ MIN(estimatedSize.height, layoutConstraints.maximumSize.height)
61
+ };
59
62
  }
60
63
  } else {
61
64
  // on the very first call there is no componentView that we can query for the component height
@@ -118,16 +118,22 @@
118
118
  [self removeAttributes:_input->textView.selectedRange];
119
119
  }
120
120
 
121
- // removing first quote line by backspacing doesn't remove typing attributes because it doesn't run textViewDidChange
122
- // so we try guessing that a point should be deleted here
123
121
  - (BOOL)handleBackspaceInRange:(NSRange)range replacementText:(NSString *)text {
124
- if([self detectStyle:_input->textView.selectedRange] &&
125
- NSEqualRanges(_input->textView.selectedRange, NSMakeRange(0, 0)) &&
126
- [text isEqualToString:@""]
127
- ) {
122
+ if([self detectStyle:_input->textView.selectedRange] && text.length == 0) {
123
+ // backspace while the style is active
124
+
128
125
  NSRange paragraphRange = [_input->textView.textStorage.string paragraphRangeForRange:_input->textView.selectedRange];
129
- [self removeAttributes:paragraphRange];
130
- return YES;
126
+
127
+ if(NSEqualRanges(_input->textView.selectedRange, NSMakeRange(0, 0))) {
128
+ // a backspace on the very first input's line quote
129
+ // it doesn't run textVieDidChange so we need to manually remove attributes
130
+ [self removeAttributes:paragraphRange];
131
+ return YES;
132
+ } else if(range.location == paragraphRange.location - 1) {
133
+ // same case in other lines; here, the removed range location will be exactly 1 less than paragraph range location
134
+ [self removeAttributes:paragraphRange];
135
+ return YES;
136
+ }
131
137
  }
132
138
  return NO;
133
139
  }
@@ -145,22 +151,11 @@
145
151
  }
146
152
  ];
147
153
  } else {
148
- NSInteger searchLocation = range.location;
149
- if(searchLocation == _input->textView.textStorage.length) {
150
- NSParagraphStyle *pStyle = _input->textView.typingAttributes[NSParagraphStyleAttributeName];
151
- return [self styleCondition:pStyle :NSMakeRange(0, 0)];
152
- }
153
-
154
- NSRange paragraphRange = NSMakeRange(0, 0);
155
- NSRange inputRange = NSMakeRange(0, _input->textView.textStorage.length);
156
- NSParagraphStyle *paragraph = [_input->textView.textStorage
157
- attribute:NSParagraphStyleAttributeName
158
- atIndex:searchLocation
159
- longestEffectiveRange: &paragraphRange
160
- inRange:inputRange
154
+ return [OccurenceUtils detect:NSParagraphStyleAttributeName withInput:_input atIndex:range.location checkPrevious:YES
155
+ withCondition:^BOOL(id _Nullable value, NSRange range) {
156
+ return [self styleCondition:value :range];
157
+ }
161
158
  ];
162
-
163
- return [self styleCondition:paragraph :NSMakeRange(0, 0)];
164
159
  }
165
160
  }
166
161