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
14 changes: 8 additions & 6 deletions .playwright/tests/mentions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ test('moving within the same mention does not re-fire onMentionDetected', async
await expect(detectedCount(page)).toHaveText('1');
});

test('moving out of a mention does not increment detected count', async ({
test('moving out of a mention fires clear onMentionDetected', async ({
page,
}) => {
await gotoMentionTest(page);
Expand All @@ -244,13 +244,15 @@ test('moving out of a mention does not increment detected count', async ({
)
.toBe(true);
await editor.click();
await editor.press('Home');
await editor.press('ArrowRight');
await editor.press('ArrowRight');
await editor.press('ArrowRight');
await editor.press('End');
await editor.press('Enter');
await editor.press('ArrowLeft'); // skip trailing space after mention
await editor.press('ArrowLeft'); // caret inside mention text
await expect(detectedCount(page)).toHaveText('1');
await editor.press('End');
await editor.press('Enter');
await expect(detectedCount(page)).toHaveText('2');
await expect(detectedText(page)).toHaveText('');
await expect(detectedIndicator(page)).toHaveText('');
});

test('mention renders correctly', async ({ page }) => {
Expand Down
4 changes: 2 additions & 2 deletions .playwright/tests/testLinks.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -347,8 +347,8 @@ test.describe('test-links onLinkDetected', () => {
.toEqual({
text: '',
url: '',
start: 8,
end: 8,
start: 0,
end: 0,
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -190,38 +190,99 @@ class EnrichedSelection(
private fun <T> getParametrizedStyleStart(type: Class<T>): Int? {
val (start, end) = getInlineSelection()
val spannable = view.text as Spannable
val spans = spannable.getSpans(start, end, type)
val isLinkType = type == EnrichedInputLinkSpan::class.java
val isMentionType = type == EnrichedInputMentionSpan::class.java

if (isLinkType && spans.isEmpty()) {
emitLinkDetectedEvent(spannable, null, start, end)
if (isMentionType) {
val activeMention = findActiveMentionSpan(spannable, start, end)
if (activeMention != null) {
val (span, spanStart, spanEnd) = activeMention
emitMentionDetectedEvent(span, spanStart, spanEnd)
return spanStart
}
if (wasMentionPreviouslyDetected()) {
emitMentionClearedEvent()
}
return null
}

if (isMentionType && spans.isEmpty()) {
emitMentionDetectedEvent(spannable, null, start, end)
if (isLinkType) {
val activeLink = findActiveLinkSpan(spannable, start, end)
if (activeLink != null) {
val (span, spanStart, spanEnd) = activeLink
emitLinkDetectedEvent(span, spanStart, spanEnd)
return spanStart
}
if (wasLinkPreviouslyDetected()) {
emitLinkClearedEvent()
}
return null
}

val spans = spannable.getSpans(start, end, type)

for (span in spans) {
val spanStart = spannable.getSpanStart(span)
val spanEnd = spannable.getSpanEnd(span)

if (start >= spanStart && end <= spanEnd) {
if (isLinkType && span is EnrichedInputLinkSpan) {
emitLinkDetectedEvent(spannable, span, spanStart, spanEnd)
} else if (isMentionType && span is EnrichedInputMentionSpan) {
emitMentionDetectedEvent(spannable, span, spanStart, spanEnd)
}

return spanStart
}
}

return null
}

private fun findActiveLinkSpan(
spannable: Spannable,
start: Int,
end: Int,
): Triple<EnrichedInputLinkSpan, Int, Int>? {
val spans = spannable.getSpans(start, end, EnrichedInputLinkSpan::class.java)

for (span in spans) {
val spanStart = spannable.getSpanStart(span)
val spanEnd = spannable.getSpanEnd(span)

if (start >= spanStart && end <= spanEnd) {
return Triple(span, spanStart, spanEnd)
}
}

return null
}

private fun findActiveMentionSpan(
spannable: Spannable,
start: Int,
end: Int,
): Triple<EnrichedInputMentionSpan, Int, Int>? {
val spans = spannable.getSpans(start, end, EnrichedInputMentionSpan::class.java)

for (span in spans) {
val spanStart = spannable.getSpanStart(span)
val spanEnd = spannable.getSpanEnd(span)

if (start >= spanStart && end <= spanEnd) {
return Triple(span, spanStart, spanEnd)
}
}

return null
}

private fun wasMentionPreviouslyDetected(): Boolean {
val previousText = previousMentionDetectedEvent["text"] ?: ""
val previousIndicator = previousMentionDetectedEvent["indicator"] ?: ""
return previousText.isNotEmpty() || previousIndicator.isNotEmpty()
}

private fun wasLinkPreviouslyDetected(): Boolean {
val previousText = previousLinkDetectedEvent["text"] ?: ""
val previousUrl = previousLinkDetectedEvent["url"] ?: ""
return previousText.isNotEmpty() || previousUrl.isNotEmpty()
}

private fun emitSelectionChangeEvent(
editable: Editable?,
start: Int,
Expand Down Expand Up @@ -249,15 +310,27 @@ class EnrichedSelection(
}

private fun emitLinkDetectedEvent(
spannable: Spannable,
span: EnrichedInputLinkSpan?,
span: EnrichedInputLinkSpan,
spanStart: Int,
spanEnd: Int,
) {
val spannable = view.text as Spannable
val text = spannable.substring(spanStart, spanEnd).replace(EnrichedConstants.ZWS_STRING, "")
dispatchLinkDetectedEvent(text, span.getUrl(), spanStart, spanEnd, spannable)
}

private fun emitLinkClearedEvent() {
val spannable = view.text as Spannable
dispatchLinkDetectedEvent("", "", 0, 0, spannable)
}

private fun dispatchLinkDetectedEvent(
text: String,
url: String,
start: Int,
end: Int,
spannable: Spannable,
) {
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)
Expand All @@ -283,16 +356,27 @@ class EnrichedSelection(
}

private fun emitMentionDetectedEvent(
spannable: Spannable,
span: EnrichedInputMentionSpan?,
start: Int,
end: Int,
span: EnrichedInputMentionSpan,
spanStart: Int,
spanEnd: Int,
) {
val text = spannable.substring(start, end)
val attributes = span?.getAttributes() ?: emptyMap()
val indicator = span?.getIndicator() ?: ""
val spannable = view.text as Spannable
val text = spannable.substring(spanStart, spanEnd)
val attributes = span.getAttributes()
val indicator = span.getIndicator()
val payload = JSONObject(attributes).toString()
dispatchMentionDetectedEvent(text, indicator, payload)
}

private fun emitMentionClearedEvent() {
dispatchMentionDetectedEvent("", "", "{}")
}

private fun dispatchMentionDetectedEvent(
text: String,
indicator: String,
payload: String,
) {
val previousText = previousMentionDetectedEvent["text"] ?: ""
val previousPayload = previousMentionDetectedEvent["payload"] ?: ""
val previousIndicator = previousMentionDetectedEvent["indicator"] ?: ""
Expand Down
128 changes: 80 additions & 48 deletions ios/EnrichedTextInputView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -976,10 +976,12 @@ - (void)tryUpdatingActiveStyles {
// data for onLinkDetected event
LinkData *detectedLinkData;
NSRange detectedLinkRange = NSMakeRange(0, 0);
BOOL shouldClearLink = NO;

// data for onMentionDetected event
MentionParams *detectedMentionParams = nullptr;
NSRange detectedMentionRange = NSMakeRange(0, 0);
BOOL shouldClearMention = NO;

for (NSNumber *type in stylesDict) {
StyleBase *style = stylesDict[type];
Expand Down Expand Up @@ -1011,60 +1013,69 @@ - (void)tryUpdatingActiveStyles {
}

// onLinkDetected event
if (isActive && [type intValue] == [LinkStyle getType]) {
// get the link data
LinkData *candidateLinkData;
NSRange candidateLinkRange = NSMakeRange(0, 0);
LinkStyle *linkStyleClass =
(LinkStyle *)stylesDict[@([LinkStyle getType])];
if (linkStyleClass != nullptr) {
candidateLinkData =
[linkStyleClass getLinkDataAt:textView.selectedRange.location];
candidateLinkRange =
[linkStyleClass getFullLinkRangeAt:textView.selectedRange.location];
}
if ([type intValue] == [LinkStyle getType]) {
if (isActive) {
// get the link data
LinkData *candidateLinkData;
NSRange candidateLinkRange = NSMakeRange(0, 0);
LinkStyle *linkStyleClass =
(LinkStyle *)stylesDict[@([LinkStyle getType])];
if (linkStyleClass != nullptr) {
candidateLinkData =
[linkStyleClass getLinkDataAt:textView.selectedRange.location];
candidateLinkRange = [linkStyleClass
getFullLinkRangeAt:textView.selectedRange.location];
}

if (wasActive == NO) {
// we changed selection from non-link to a link
detectedLinkData = candidateLinkData;
detectedLinkRange = candidateLinkRange;
} else if (![_recentlyActiveLinkData
isEqualToLinkData:candidateLinkData] ||
!NSEqualRanges(_recentlyActiveLinkRange, candidateLinkRange)) {
// we changed selection from one link to the other or modified
// current link's text
detectedLinkData = candidateLinkData;
detectedLinkRange = candidateLinkRange;
if (wasActive == NO) {
// we changed selection from non-link to a link
detectedLinkData = candidateLinkData;
detectedLinkRange = candidateLinkRange;
} else if (![_recentlyActiveLinkData
isEqualToLinkData:candidateLinkData] ||
!NSEqualRanges(_recentlyActiveLinkRange,
candidateLinkRange)) {
// we changed selection from one link to the other or modified
// current link's text
detectedLinkData = candidateLinkData;
detectedLinkRange = candidateLinkRange;
}
} else if (wasActive && [self wasLinkPreviouslyDetected]) {
shouldClearLink = YES;
}
}

// onMentionDetected event
if (isActive && [type intValue] == [MentionStyle getType]) {
// get mention data
MentionParams *candidateMentionParams;
NSRange candidateMentionRange = NSMakeRange(0, 0);
MentionStyle *mentionStyleClass =
(MentionStyle *)stylesDict[@([MentionStyle getType])];
if (mentionStyleClass != nullptr) {
candidateMentionParams = [mentionStyleClass
getMentionParamsAt:textView.selectedRange.location];
candidateMentionRange = [mentionStyleClass
getFullMentionRangeAt:textView.selectedRange.location];
}
if ([type intValue] == [MentionStyle getType]) {
if (isActive) {
// get mention data
MentionParams *candidateMentionParams;
NSRange candidateMentionRange = NSMakeRange(0, 0);
MentionStyle *mentionStyleClass =
(MentionStyle *)stylesDict[@([MentionStyle getType])];
if (mentionStyleClass != nullptr) {
candidateMentionParams = [mentionStyleClass
getMentionParamsAt:textView.selectedRange.location];
candidateMentionRange = [mentionStyleClass
getFullMentionRangeAt:textView.selectedRange.location];
}

if (wasActive == NO) {
// selection was changed from a non-mention to a mention
detectedMentionParams = candidateMentionParams;
detectedMentionRange = candidateMentionRange;
} else if (![_recentlyActiveMentionParams.text
isEqualToString:candidateMentionParams.text] ||
![_recentlyActiveMentionParams.attributes
isEqualToString:candidateMentionParams.attributes] ||
!NSEqualRanges(_recentlyActiveMentionRange,
candidateMentionRange)) {
// selection changed from one mention to another
detectedMentionParams = candidateMentionParams;
detectedMentionRange = candidateMentionRange;
if (wasActive == NO) {
// selection was changed from a non-mention to a mention
detectedMentionParams = candidateMentionParams;
detectedMentionRange = candidateMentionRange;
} else if (![_recentlyActiveMentionParams.text
isEqualToString:candidateMentionParams.text] ||
![_recentlyActiveMentionParams.attributes
isEqualToString:candidateMentionParams.attributes] ||
!NSEqualRanges(_recentlyActiveMentionRange,
candidateMentionRange)) {
// selection changed from one mention to another
detectedMentionParams = candidateMentionParams;
detectedMentionRange = candidateMentionRange;
}
} else if (wasActive && [self wasMentionPreviouslyDetected]) {
shouldClearMention = YES;
}
}
}
Expand Down Expand Up @@ -1111,6 +1122,11 @@ - (void)tryUpdatingActiveStyles {
if (detectedLinkData != nullptr) {
// emit onLinkeDetected event
[self emitOnLinkDetectedEvent:detectedLinkData range:detectedLinkRange];
} else if (shouldClearLink) {
LinkData *emptyLinkData = [[LinkData alloc] init];
emptyLinkData.text = @"";
emptyLinkData.url = @"";
[self emitOnLinkDetectedEvent:emptyLinkData range:NSMakeRange(0, 0)];
}

if (detectedMentionParams != nullptr) {
Expand All @@ -1121,6 +1137,10 @@ - (void)tryUpdatingActiveStyles {

_recentlyActiveMentionParams = detectedMentionParams;
_recentlyActiveMentionRange = detectedMentionRange;
} else if (shouldClearMention) {
[self emitOnMentionDetectedEvent:@"" indicator:@"" attributes:@"{}"];
_recentlyActiveMentionParams = nullptr;
_recentlyActiveMentionRange = NSMakeRange(0, 0);
}
// emit onChangeHtml event if needed
[self tryEmittingOnChangeHtmlEvent];
Expand Down Expand Up @@ -1327,6 +1347,18 @@ - (void)emitOnSubmitEdittingEvent {
}
}

- (BOOL)wasLinkPreviouslyDetected {
return _recentlyActiveLinkData != nullptr &&
(_recentlyActiveLinkData.text.length > 0 ||
_recentlyActiveLinkData.url.length > 0);
}

- (BOOL)wasMentionPreviouslyDetected {
return _recentlyActiveMentionParams != nullptr &&
(_recentlyActiveMentionParams.text.length > 0 ||
_recentlyActiveMentionParams.indicator.length > 0);
}

- (void)emitOnLinkDetectedEvent:(LinkData *)linkData range:(NSRange)range {
auto emitter = [self getEventEmitter];
if (emitter != nullptr) {
Expand Down
Loading
Loading