diff --git a/.playwright/tests/mentions.spec.ts b/.playwright/tests/mentions.spec.ts index d8bc60dc7..5e6d4379d 100644 --- a/.playwright/tests/mentions.spec.ts +++ b/.playwright/tests/mentions.spec.ts @@ -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); @@ -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 }) => { diff --git a/.playwright/tests/testLinks.spec.ts b/.playwright/tests/testLinks.spec.ts index dd6208f15..82bef7368 100644 --- a/.playwright/tests/testLinks.spec.ts +++ b/.playwright/tests/testLinks.spec.ts @@ -347,8 +347,8 @@ test.describe('test-links onLinkDetected', () => { .toEqual({ text: '', url: '', - start: 8, - end: 8, + start: 0, + end: 0, }); }); }); 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 64239d295..bce77da1c 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 @@ -195,12 +195,16 @@ class EnrichedSelection( val isMentionType = type == EnrichedInputMentionSpan::class.java if (isLinkType && spans.isEmpty()) { - emitLinkDetectedEvent(spannable, null, start, end) + if (wasLinkPreviouslyDetected()) { + emitLinkDetectedEvent(spannable, null, 0, 0) + } return null } if (isMentionType && spans.isEmpty()) { - emitMentionDetectedEvent(spannable, null, start, end) + if (wasMentionPreviouslyDetected()) { + emitMentionDetectedEvent(spannable, null, start, end) + } return null } @@ -248,6 +252,18 @@ class EnrichedSelection( ) } + 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 emitLinkDetectedEvent( spannable: Spannable, span: EnrichedInputLinkSpan?, diff --git a/ios/EnrichedTextInputView.mm b/ios/EnrichedTextInputView.mm index 95b8bd5c4..a0d3d6eb2 100644 --- a/ios/EnrichedTextInputView.mm +++ b/ios/EnrichedTextInputView.mm @@ -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]; @@ -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) { + 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) { + shouldClearMention = YES; } } } @@ -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) { @@ -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]; diff --git a/src/web/pmPlugins/MentionPlugin/subscribeMentionEvents.ts b/src/web/pmPlugins/MentionPlugin/subscribeMentionEvents.ts index 79ef6d98e..36fbdba51 100644 --- a/src/web/pmPlugins/MentionPlugin/subscribeMentionEvents.ts +++ b/src/web/pmPlugins/MentionPlugin/subscribeMentionEvents.ts @@ -10,6 +10,7 @@ export function subscribeMentionEvents( ): () => void { let prevTriggerState: TriggerState = { active: false }; let prevMentionKey: string | null = null; + let wasInMention = false; const handleTransaction = () => { const cb = getCallbacks(); @@ -31,14 +32,28 @@ export function subscribeMentionEvents( } prevTriggerState = curr; - const mention = cb.onMentionDetected ? getActiveMention(editor) : null; + if (!cb.onMentionDetected) return; + + const mention = getActiveMention(editor); if (!mention) { - prevMentionKey = null; + if (wasInMention) { + wasInMention = false; + prevMentionKey = null; + cb.onMentionDetected({ + text: '', + indicator: '', + attributes: {}, + }); + } else { + prevMentionKey = null; + } return; } + + wasInMention = true; if (mention.key === prevMentionKey) return; prevMentionKey = mention.key; - cb.onMentionDetected?.({ + cb.onMentionDetected({ text: mention.text, indicator: mention.indicator, attributes: mention.attributes, @@ -68,15 +83,18 @@ function getActiveMention( ): (OnMentionDetected & { key: string }) | null { const { state } = editor; const mentionType = state.schema.marks.mention; - if (!mentionType || !state.selection.empty) return null; + if (!mentionType) return null; - const $pos = state.doc.resolve(state.selection.from); - const mark = mentionType.isInSet($pos.marks()); + const { from: selFrom, to: selTo } = state.selection; + const $from = state.doc.resolve(selFrom); + const mark = mentionType.isInSet($from.marks()); if (!mark) return null; - const range = getMarkRange($pos, mentionType); + const range = getMarkRange($from, mentionType); if (!range) return null; + if (selFrom < range.from || selTo > range.to) return null; + const { text, indicator, attributes } = mark.attrs; return { key: `${range.from}:${range.to}:${text}:${indicator}`, diff --git a/src/web/useOnLinkDetected.ts b/src/web/useOnLinkDetected.ts index c45907f51..daf1fb004 100644 --- a/src/web/useOnLinkDetected.ts +++ b/src/web/useOnLinkDetected.ts @@ -19,7 +19,6 @@ export const useOnLinkDetected = ( const linkType = state.schema.marks.link; if (!linkType) return; - const { from: selFrom, to: selTo } = state.selection; const $pos = state.selection.$from; const range = getMarkRange($pos, linkType); @@ -29,8 +28,8 @@ export const useOnLinkDetected = ( onLinkDetected({ text: '', url: '', - start: tiptapPosToNativePos(state.doc, selFrom), - end: tiptapPosToNativePos(state.doc, selTo), + start: 0, + end: 0, }); } lastEmittedRef.current = null;