From 098053a32bade1a90425316518a8da0a95dc141f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Mon, 1 Jun 2026 12:32:55 +0200 Subject: [PATCH 1/6] fix(android): split manual links when newline inserted in link --- .../textinput/styles/ParametrizedStyles.kt | 66 +++++++++++++++++-- .../textinput/utils/EnrichedSelection.kt | 25 +++++-- 2 files changed, 80 insertions(+), 11 deletions(-) diff --git a/android/src/main/java/com/swmansion/enriched/textinput/styles/ParametrizedStyles.kt b/android/src/main/java/com/swmansion/enriched/textinput/styles/ParametrizedStyles.kt index 82a7922d..50af33ce 100644 --- a/android/src/main/java/com/swmansion/enriched/textinput/styles/ParametrizedStyles.kt +++ b/android/src/main/java/com/swmansion/enriched/textinput/styles/ParametrizedStyles.kt @@ -61,10 +61,7 @@ class ParametrizedStyles( } val spanEnd = start + text.length - val span = EnrichedInputLinkSpan(url, view.htmlStyle, true) - val (safeStart, safeEnd) = spannable.getSafeSpanBoundaries(start, spanEnd) - spannable.setSpan(span, safeStart, safeEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) - + applyManualLinkSpan(spannable, start, spanEnd, url) view.selection?.validateStyles() isSettingLinkSpan = false } @@ -90,6 +87,7 @@ class ParametrizedStyles( startCursorPosition: Int, endCursorPosition: Int, ) { + afterTextChangedManualLinks(startCursorPosition, endCursorPosition) afterTextChangedLinks(startCursorPosition, endCursorPosition) afterTextChangedMentions(s, startCursorPosition) } @@ -227,6 +225,66 @@ class ParametrizedStyles( return true } + private fun afterTextChangedManualLinks( + editStart: Int, + editEnd: Int, + ) { + if (isSettingLinkSpan || editEnd <= editStart) return + val spannable = view.text as? Spannable ?: return + + val inserted = spannable.subSequence(editStart, editEnd) + if (inserted.none { it == '\n' }) return + + spannable + .getSpans(editStart, editEnd, EnrichedInputLinkSpan::class.java) + .filter { it.getIsManual() } + .distinct() + .forEach { splitManualLinkOnNewlines(spannable, it) } + } + + private fun splitManualLinkOnNewlines( + spannable: Spannable, + span: EnrichedInputLinkSpan, + ) { + val spanStart = spannable.getSpanStart(span) + val spanEnd = spannable.getSpanEnd(span) + if (spanStart < 0 || spanEnd <= spanStart) return + + val segment = spannable.subSequence(spanStart, spanEnd) + if (segment.none { it == '\n' || it == '\r' }) return + + val url = span.getUrl() + spannable.removeSpan(span) + + var runStart = spanStart + for (i in spanStart until spanEnd) { + if (spannable[i] == '\n' || spannable[i] == '\r') { + if (runStart < i) { + applyManualLinkSpan(spannable, runStart, i, url) + } + runStart = i + 1 + } + } + if (runStart < spanEnd) { + applyManualLinkSpan(spannable, runStart, spanEnd, url) + } + } + + private fun applyManualLinkSpan( + spannable: Spannable, + start: Int, + end: Int, + url: String, + ) { + val (safeStart, safeEnd) = spannable.getSafeSpanBoundaries(start, end) + spannable.setSpan( + EnrichedInputLinkSpan(url, view.htmlStyle, true), + safeStart, + safeEnd, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE, + ) + } + private fun afterTextChangedLinks( editStart: Int, editEnd: Int, diff --git a/android/src/main/java/com/swmansion/enriched/textinput/utils/EnrichedSelection.kt b/android/src/main/java/com/swmansion/enriched/textinput/utils/EnrichedSelection.kt index 64239d29..b3671cc4 100644 --- a/android/src/main/java/com/swmansion/enriched/textinput/utils/EnrichedSelection.kt +++ b/android/src/main/java/com/swmansion/enriched/textinput/utils/EnrichedSelection.kt @@ -20,7 +20,8 @@ class EnrichedSelection( var start: Int = 0 var end: Int = 0 - private var previousLinkDetectedEvent: MutableMap = mutableMapOf("text" to "", "url" to "") + private var previousLinkDetectedEvent: MutableMap = + mutableMapOf("text" to "", "url" to "", "start" to "", "end" to "") private var previousMentionDetectedEvent: MutableMap = mutableMapOf("text" to "", "payload" to "") fun onSelection( @@ -257,14 +258,24 @@ class EnrichedSelection( val text = spannable.substring(start, end).replace(EnrichedConstants.ZWS_STRING, "") val url = span?.getUrl() ?: "" - // Prevents emitting unnecessary events - if (text == previousLinkDetectedEvent["text"] && url == previousLinkDetectedEvent["url"]) return - - previousLinkDetectedEvent.put("text", text) - previousLinkDetectedEvent.put("url", url) - val visibleStart = start - spannable.zwsCountBefore(start) val visibleEnd = end - spannable.zwsCountBefore(end) + val visibleStartString = visibleStart.toString() + val visibleEndString = visibleEnd.toString() + + // Prevents emitting unnecessary events + if (text == previousLinkDetectedEvent["text"] && + url == previousLinkDetectedEvent["url"] && + visibleStartString == previousLinkDetectedEvent["start"] && + visibleEndString == previousLinkDetectedEvent["end"] + ) { + return + } + + previousLinkDetectedEvent["text"] = text + previousLinkDetectedEvent["url"] = url + previousLinkDetectedEvent["start"] = visibleStartString + previousLinkDetectedEvent["end"] = visibleEndString val context = view.context as ReactContext val surfaceId = UIManagerHelper.getSurfaceId(context) From 6426ab4a529b9abddf15c7a60fb721f13df93400 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Tue, 2 Jun 2026 11:36:23 +0200 Subject: [PATCH 2/6] fix(iOS): split manual link into multiple links --- ios/EnrichedTextInputView.mm | 7 +++++-- ios/styles/LinkStyle.mm | 5 ++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/ios/EnrichedTextInputView.mm b/ios/EnrichedTextInputView.mm index 7c7505ca..5ae0cf7d 100644 --- a/ios/EnrichedTextInputView.mm +++ b/ios/EnrichedTextInputView.mm @@ -1591,8 +1591,11 @@ - (void)manageSelectionBasedChanges { [attributesManager manageTypingAttributesWithOnlySelection:onlySelectionChanged]; - // always update active styles - [self tryUpdatingActiveStyles]; + // When text changed, anyTextMayHaveBeenModified runs tryUpdatingActiveStyles + if ([_recentInputString isEqualToString:currentString]) { + // always update active styles + [self tryUpdatingActiveStyles]; + } } - (void)handleWordModificationBasedChanges:(NSString *)word diff --git a/ios/styles/LinkStyle.mm b/ios/styles/LinkStyle.mm index ef600c6c..a9b79a75 100644 --- a/ios/styles/LinkStyle.mm +++ b/ios/styles/LinkStyle.mm @@ -436,7 +436,10 @@ - (void)handleManualLinks:(NSString *)word inRange:(NSRange)wordRange { if ([manualLinkMinValue isEqualToLinkData:manualLinkMaxValue]) { NSRange newRange = NSMakeRange(manualLinkMinIdx, manualLinkMaxIdx - manualLinkMinIdx + 1); - [self applyLinkMetaWithData:manualLinkMinValue range:newRange]; + LinkData *updatedData = [manualLinkMinValue copy]; + updatedData.text = + [self.host.textView.textStorage.string substringWithRange:newRange]; + [self applyLinkMetaWithData:updatedData range:newRange]; [self.host.attributesManager addDirtyRange:newRange]; } } From 1bde239bcfb8ad0334cca8b8d44094ec1431794d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Tue, 2 Jun 2026 11:50:29 +0200 Subject: [PATCH 3/6] fix: refactor splitting manual links --- .../textinput/styles/ParametrizedStyles.kt | 29 +++++-------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/android/src/main/java/com/swmansion/enriched/textinput/styles/ParametrizedStyles.kt b/android/src/main/java/com/swmansion/enriched/textinput/styles/ParametrizedStyles.kt index 50af33ce..9012deef 100644 --- a/android/src/main/java/com/swmansion/enriched/textinput/styles/ParametrizedStyles.kt +++ b/android/src/main/java/com/swmansion/enriched/textinput/styles/ParametrizedStyles.kt @@ -229,16 +229,13 @@ class ParametrizedStyles( editStart: Int, editEnd: Int, ) { - if (isSettingLinkSpan || editEnd <= editStart) return + if (isSettingLinkSpan) return val spannable = view.text as? Spannable ?: return - - val inserted = spannable.subSequence(editStart, editEnd) - if (inserted.none { it == '\n' }) return + if (spannable.subSequence(editStart, editEnd).none { it == '\n' || it == '\r' }) return spannable .getSpans(editStart, editEnd, EnrichedInputLinkSpan::class.java) .filter { it.getIsManual() } - .distinct() .forEach { splitManualLinkOnNewlines(spannable, it) } } @@ -246,28 +243,18 @@ class ParametrizedStyles( spannable: Spannable, span: EnrichedInputLinkSpan, ) { - val spanStart = spannable.getSpanStart(span) - val spanEnd = spannable.getSpanEnd(span) - if (spanStart < 0 || spanEnd <= spanStart) return - - val segment = spannable.subSequence(spanStart, spanEnd) - if (segment.none { it == '\n' || it == '\r' }) return - + val start = spannable.getSpanStart(span) + val end = spannable.getSpanEnd(span) val url = span.getUrl() spannable.removeSpan(span) - var runStart = spanStart - for (i in spanStart until spanEnd) { - if (spannable[i] == '\n' || spannable[i] == '\r') { - if (runStart < i) { - applyManualLinkSpan(spannable, runStart, i, url) - } + var runStart = start + for (i in start..end) { + if (i == end || spannable[i] == '\n' || spannable[i] == '\r') { + if (runStart < i) applyManualLinkSpan(spannable, runStart, i, url) runStart = i + 1 } } - if (runStart < spanEnd) { - applyManualLinkSpan(spannable, runStart, spanEnd, url) - } } private fun applyManualLinkSpan( From 8354b17aece55d6b2f4d33c65136ce2e0e9520fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= <74975508+kacperzolkiewski@users.noreply.github.com> Date: Tue, 2 Jun 2026 12:28:33 +0200 Subject: [PATCH 4/6] Apply suggestion from @kacperzolkiewski --- ios/EnrichedTextInputView.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/EnrichedTextInputView.mm b/ios/EnrichedTextInputView.mm index 5ae0cf7d..c27ef33c 100644 --- a/ios/EnrichedTextInputView.mm +++ b/ios/EnrichedTextInputView.mm @@ -1593,7 +1593,7 @@ - (void)manageSelectionBasedChanges { // When text changed, anyTextMayHaveBeenModified runs tryUpdatingActiveStyles if ([_recentInputString isEqualToString:currentString]) { - // always update active styles + // update active styles [self tryUpdatingActiveStyles]; } } From 8f5fc01a25ce4024e52dd8b90a07695666cb7a6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Tue, 2 Jun 2026 12:57:40 +0200 Subject: [PATCH 5/6] fix: example app --- apps/example/src/hooks/useEditorState.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/example/src/hooks/useEditorState.ts b/apps/example/src/hooks/useEditorState.ts index 4327f4b6..c26911d1 100644 --- a/apps/example/src/hooks/useEditorState.ts +++ b/apps/example/src/hooks/useEditorState.ts @@ -44,7 +44,11 @@ export function useEditorState() { const [isImageModalOpen, setIsImageModalOpen] = useState(false); const [isValueModalOpen, setIsValueModalOpen] = useState(false); const [currentHtml, setCurrentHtml] = useState(''); - const [selection, setSelection] = useState(); + const [selection, setSelection] = useState({ + start: 0, + end: 0, + text: '', + }); const [stylesState, setStylesState] = useState(DEFAULT_STYLES); const [currentLink, setCurrentLink] = useState(DEFAULT_LINK_STATE); From a22fbf7b811d8e01ee8f757a036d20bbfe8f9dbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Mon, 15 Jun 2026 10:39:40 +0200 Subject: [PATCH 6/6] fix: updating recentInputString --- ios/EnrichedTextInputView.mm | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ios/EnrichedTextInputView.mm b/ios/EnrichedTextInputView.mm index ea895574..1a063d77 100644 --- a/ios/EnrichedTextInputView.mm +++ b/ios/EnrichedTextInputView.mm @@ -1692,12 +1692,11 @@ - (void)anyTextMayHaveBeenModified { } if (![textView.textStorage.string isEqualToString:_recentInputString]) { + _recentInputString = [textView.textStorage.string copy]; + // emit onChangeText event auto emitter = [self getEventEmitter]; if (emitter != nullptr && _emitTextChange) { - // set the recent input string only if the emitter is defined - _recentInputString = [textView.textStorage.string copy]; - // emit string without zero width spaces NSString *stringToBeEmitted = [[textView.textStorage.string stringByReplacingOccurrencesOfString:@"\u200B"