Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,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, EnrichedSpanFlags.forSpan(span))

applyManualLinkSpan(spannable, start, spanEnd, url)
view.selection?.validateStyles()
isSettingLinkSpan = false
}
Expand All @@ -93,6 +90,7 @@ class ParametrizedStyles(
startCursorPosition: Int,
endCursorPosition: Int,
) {
afterTextChangedManualLinks(startCursorPosition, endCursorPosition)
afterTextChangedLinks(startCursorPosition, endCursorPosition)
detectActiveMention(s, startCursorPosition)
}
Expand Down Expand Up @@ -246,6 +244,54 @@ 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)
val span = EnrichedInputLinkSpan(url, view.htmlStyle, true)
spannable.setSpan(
span,
safeStart,
safeEnd,
EnrichedSpanFlags.forSpan(span),
)
}

private fun afterTextChangedLinks(
editStart: Int,
editEnd: Int,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ class EnrichedSelection(
var start: Int = 0
var end: Int = 0

private var previousLinkDetectedEvent: MutableMap<String, String> = mutableMapOf("text" to "", "url" to "")
private var previousLinkDetectedEvent: MutableMap<String, String> =
mutableMapOf("text" to "", "url" to "", "start" to "", "end" to "")
private var previousMentionDetectedEvent: MutableMap<String, String> = mutableMapOf("text" to "", "payload" to "")

fun onSelection(
Expand Down Expand Up @@ -311,6 +312,15 @@ class EnrichedSelection(
)
}

private fun isSelectionOutdated(): Boolean {
val actualStart = minOf(view.selectionStart, view.selectionEnd)
val actualEnd = maxOf(view.selectionStart, view.selectionEnd)
if (actualStart < 0) return false

val (currentStart, currentEnd) = getInlineSelection()
return actualStart != currentStart || actualEnd != currentEnd
}

private fun wasMentionPreviouslyDetected(): Boolean {
val previousText = previousMentionDetectedEvent["text"] ?: ""
val previousIndicator = previousMentionDetectedEvent["indicator"] ?: ""
Expand All @@ -329,17 +339,31 @@ class EnrichedSelection(
start: Int,
end: Int,
) {
// While text is being changed, styles get validated before onSelection receives the new cursor position and before manual links are split on newlines.
// A link event emitted then would be stale; onSelection validates again once the selection is updated.
if (isSelectionOutdated()) return

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)
Expand Down
6 changes: 5 additions & 1 deletion apps/example/src/hooks/useEditorState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Selection>();
const [selection, setSelection] = useState<Selection>({
start: 0,
end: 0,
text: '',
});
const [stylesState, setStylesState] = useState<StylesState>(DEFAULT_STYLES);
const [currentLink, setCurrentLink] =
useState<CurrentLinkState>(DEFAULT_LINK_STATE);
Expand Down
19 changes: 17 additions & 2 deletions ios/EnrichedTextInputView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -1076,6 +1076,18 @@ - (void)tryUpdatingActiveStyles {
getFullLinkRangeAt:textView.selectedRange.location];
}

// Stored link text is only refreshed for automatic links (which are
// single words). Manual links keep the text they were created with: it
// must stay identical along the whole link so that
// longestEffectiveRange finds its full extent, while edits are handled
// per word. So always read the current text from the detected range.
if (candidateLinkData != nullptr && candidateLinkRange.length > 0 &&
NSMaxRange(candidateLinkRange) <= textView.textStorage.length) {
candidateLinkData = [candidateLinkData copy];
candidateLinkData.text = [textView.textStorage.string
substringWithRange:candidateLinkRange];
}

if (wasActive == NO) {
// we changed selection from non-link to a link
detectedLinkData = candidateLinkData;
Expand Down Expand Up @@ -1745,8 +1757,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];
}
Comment thread
kacperzolkiewski marked this conversation as resolved.
Resolved
}

- (void)handleWordModificationBasedChanges:(NSString *)word
Expand Down
Loading