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..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 @@ -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,53 @@ class ParametrizedStyles( return true } + private fun afterTextChangedManualLinks( + editStart: Int, + editEnd: Int, + ) { + if (isSettingLinkSpan) return + val spannable = view.text as? Spannable ?: return + if (spannable.subSequence(editStart, editEnd).none { it == '\n' || it == '\r' }) return + + spannable + .getSpans(editStart, editEnd, EnrichedInputLinkSpan::class.java) + .filter { it.getIsManual() } + .forEach { splitManualLinkOnNewlines(spannable, it) } + } + + private fun splitManualLinkOnNewlines( + spannable: Spannable, + span: EnrichedInputLinkSpan, + ) { + val start = spannable.getSpanStart(span) + val end = spannable.getSpanEnd(span) + val url = span.getUrl() + spannable.removeSpan(span) + + 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 + } + } + } + + 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 bce77da1..ed20a1ed 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( @@ -273,14 +274,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) 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); diff --git a/ios/EnrichedTextInputView.mm b/ios/EnrichedTextInputView.mm index b9487825..1a063d77 100644 --- a/ios/EnrichedTextInputView.mm +++ b/ios/EnrichedTextInputView.mm @@ -1611,8 +1611,11 @@ - (void)manageSelectionBasedChanges { [attributesManager manageTypingAttributesWithOnlySelection:onlySelectionChanged]; - // always update active styles - [self tryUpdatingActiveStyles]; + // When text changed, anyTextMayHaveBeenModified runs tryUpdatingActiveStyles + if ([_recentInputString isEqualToString:currentString]) { + // update active styles + [self tryUpdatingActiveStyles]; + } } - (void)handleWordModificationBasedChanges:(NSString *)word @@ -1689,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" 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]; } }