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 @@ -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
}

Expand Down Expand Up @@ -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?,
Expand Down
116 changes: 68 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) {
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;
}
}
}
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
32 changes: 25 additions & 7 deletions src/web/pmPlugins/MentionPlugin/subscribeMentionEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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,
Expand Down Expand Up @@ -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}`,
Expand Down
5 changes: 2 additions & 3 deletions src/web/useOnLinkDetected.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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;
Expand Down
Loading