feat(editor): 添加鼠标点击选择mention功能并优化插入逻辑

- 新增handleMentionSelectByMouse函数处理鼠标点击选择mention
- 重构insertMention函数,支持传入range参数并优化插入逻辑
- 修复mention列表点击事件,防止默认行为导致的问题
- 优化onSubscribeMention函数,确保焦点和选区正确处理
This commit is contained in:
Phoenix 2025-06-10 09:45:10 +08:00
parent bdf07155c8
commit d4e52152ef

View File

@ -174,6 +174,23 @@ const showMentionList = () => {
selectedMentionIndex.value = 0 selectedMentionIndex.value = 0
} }
// mention
const handleMentionSelectByMouse = (member) => {
const selection = window.getSelection();
if (selection && selection.rangeCount > 0) {
insertMention(member, selection.getRangeAt(0).cloneRange());
} else {
//
editorRef.value?.focus();
nextTick(() => {
const newSelection = window.getSelection();
if (newSelection && newSelection.rangeCount > 0) {
insertMention(member, newSelection.getRangeAt(0).cloneRange());
}
});
}
};
// mention // mention
const hideMentionList = () => { const hideMentionList = () => {
showMention.value = false showMention.value = false
@ -193,67 +210,75 @@ const updateMentionPosition = (range) => {
} }
// mention // mention
const insertMention = (member) => { const insertMention = (member, clonedRange) => {
console.log('插入mention',member) console.log('插入mention', member);
const selection = window.getSelection() const selection = window.getSelection();
if (!selection.rangeCount) return if (!clonedRange || !selection) return;
const range = selection.getRangeAt(0) const range = clonedRange; // 使 range
const textNode = range.startContainer
const offset = range.startOffset
// @ const textNode = range.startContainer;
const textContent = textNode.textContent || '' const offset = range.startOffset;
const atIndex = textContent.lastIndexOf('@', offset - 1) const textContent = textNode.nodeType === Node.TEXT_NODE ? textNode.textContent || '' : '';
// @
const atIndex = (textNode.nodeType === Node.TEXT_NODE && offset > 0) ? textContent.lastIndexOf('@', offset - 1) : -1;
// mention const mentionSpan = document.createElement('span');
const mentionSpan = document.createElement('span') mentionSpan.className = 'mention';
mentionSpan.className = 'mention' mentionSpan.setAttribute('data-user-id', String(member.id));
mentionSpan.setAttribute('data-user-id',String(member.id)) mentionSpan.textContent = `@${member.value || member.nickname}`;
mentionSpan.textContent = `@${member.value || member.nickname}` mentionSpan.contentEditable = 'false';
mentionSpan.contentEditable = 'false'
if (atIndex !== -1) { if (atIndex !== -1 && textNode.nodeType === Node.TEXT_NODE) {
// @ const parent = textNode.parentNode;
const beforeText = textContent.substring(0, atIndex) if (!parent) return; // Sanity check
const afterText = textContent.substring(offset)
// const textBeforeAt = textContent.substring(0, atIndex);
const beforeNode = document.createTextNode(beforeText) const textAfterCursor = textContent.substring(offset);
const afterNode = document.createTextNode(' ' + afterText)
// const beforeNode = document.createTextNode(textBeforeAt);
const parent = textNode.parentNode const spaceAfterMentionNode = document.createTextNode(' '); // Ensure space after mention
parent.insertBefore(beforeNode, textNode) const afterNode = document.createTextNode(textAfterCursor);
parent.insertBefore(mentionSpan, textNode)
parent.insertBefore(afterNode, textNode) parent.insertBefore(beforeNode, textNode);
parent.removeChild(textNode) parent.insertBefore(mentionSpan, textNode);
parent.insertBefore(spaceAfterMentionNode, textNode);
parent.insertBefore(afterNode, textNode);
parent.removeChild(textNode);
range.setStartAfter(spaceAfterMentionNode);
range.collapse(true);
} else { } else {
// @ // @
range.deleteContents() if (!range.collapsed) {
range.deleteContents();
// @
range.insertNode(mentionSpan)
// @
const spaceNode = document.createTextNode(' ')
range.setStartAfter(mentionSpan)
range.insertNode(spaceNode)
//
range.setStartAfter(spaceNode)
range.collapse(true)
selection.removeAllRanges()
selection.addRange(range)
} }
// range.insertNode(mentionSpan);
handleInput({ target: editorRef.value })
// mention const spaceNode = document.createTextNode(' ');
hideMentionList() // range mentionSpan
const tempRangeForSpace = range.cloneRange(); // 使 range range
tempRangeForSpace.setStartAfter(mentionSpan);
tempRangeForSpace.collapse(true);
tempRangeForSpace.insertNode(spaceNode);
// range
range.setStartAfter(spaceNode);
range.collapse(true);
} }
selection.removeAllRanges();
selection.addRange(range);
editorRef.value?.focus(); //
nextTick(() => {
handleInput({ target: editorRef.value });
hideMentionList();
});
};
// //
const handlePaste = (event) => { const handlePaste = (event) => {
event.preventDefault() event.preventDefault()
@ -350,12 +375,15 @@ const handleKeydown = (event) => {
break break
case 'Enter': case 'Enter':
case 'Tab': case 'Tab':
event.preventDefault() event.preventDefault();
const selectedMember = mentionList.value[selectedMentionIndex.value] const selectedMember = mentionList.value[selectedMentionIndex.value];
if (selectedMember) { if (selectedMember) {
insertMention(selectedMember) const selection = window.getSelection();
if (selection && selection.rangeCount > 0) {
insertMention(selectedMember, selection.getRangeAt(0).cloneRange());
} }
break }
break;
case 'Escape': case 'Escape':
hideMentionList() hideMentionList()
break break
@ -398,6 +426,25 @@ const handleKeydown = (event) => {
} }
prevSibling = prevSibling.previousSibling prevSibling = prevSibling.previousSibling
} }
} else {
//
// mention
if (!container.textContent.trim()) {
let prevSibling = container.previousSibling
while (prevSibling) {
if (prevSibling.nodeType === Node.ELEMENT_NODE &&
prevSibling.classList &&
prevSibling.classList.contains('mention')) {
targetMention = prevSibling
break
}
//
if (prevSibling.nodeType === Node.TEXT_NODE && prevSibling.textContent.trim()) {
break
}
prevSibling = prevSibling.previousSibling
}
}
} }
} else if (container.nodeType === Node.ELEMENT_NODE) { } else if (container.nodeType === Node.ELEMENT_NODE) {
// //
@ -930,27 +977,44 @@ const insertImageEmoji = (imgSrc, altText) => {
} }
// //
const onSubscribeMention = (data) => { const onSubscribeMention = async (data) => {
// const editorNode = editorRef.value;
editorRef.value?.focus() if (!editorNode) return;
// editorNode.focus();
const selection = window.getSelection() await nextTick(); //
if (!selection.rangeCount || !editorRef.value.contains(selection.anchorNode)) {
const range = document.createRange() let selection = window.getSelection();
if (editorRef.value.lastChild) { if (!selection || selection.rangeCount === 0) {
range.setStartAfter(editorRef.value.lastChild) const range = document.createRange();
if (editorNode.lastChild) {
range.setStartAfter(editorNode.lastChild);
} else { } else {
range.setStart(editorRef.value, 0) range.setStart(editorNode, 0);
} }
range.collapse(true) range.collapse(true);
selection.removeAllRanges() if (selection) selection.removeAllRanges();
selection.addRange(range) selection?.addRange(range);
await nextTick();
selection = window.getSelection();
} else if (!editorNode.contains(selection.anchorNode)) {
const range = document.createRange();
if (editorNode.lastChild) {
range.setStartAfter(editorNode.lastChild);
} else {
range.setStart(editorNode, 0);
}
range.collapse(true);
selection.removeAllRanges();
selection.addRange(range);
await nextTick();
selection = window.getSelection();
} }
// @ if (selection && selection.rangeCount > 0) {
insertMention(data) insertMention(data, selection.getRangeAt(0).cloneRange());
} }
};
const onSubscribeQuote = (data) => { const onSubscribeQuote = (data) => {
// 稿 // 稿
@ -1443,7 +1507,7 @@ const handleEditorClick = (event) => {
<n-icon size="18" class="icon" :component="nav.icon" /> <n-icon size="18" class="icon" :component="nav.icon" />
<p class="tip-title">{{ nav.title }}</p> <p class="tip-title">{{ nav.title }}</p>
</div> </div>
<n-button class="w-80px h-30px ml-auto" type="primary"> <n-button class="w-80px h-30px ml-auto" type="primary" @click="sendMessage">
<template #icon> <template #icon>
<n-icon> <n-icon>
<IosSend /> <IosSend />
@ -1495,7 +1559,7 @@ const handleEditorClick = (event) => {
:key="member.user_id || member.id" :key="member.user_id || member.id"
class="cursor-pointer px-14px h-42px" class="cursor-pointer px-14px h-42px"
:class="{ 'bg-#EEE9F9': index === selectedMentionIndex }" :class="{ 'bg-#EEE9F9': index === selectedMentionIndex }"
@mousedown.prevent="insertMention(member)" @mousedown.prevent="handleMentionSelectByMouse(member)"
@mouseover="selectedMentionIndex = index" @mouseover="selectedMentionIndex = index"
> >
<div class="flex items-center border-b-1px border-b-solid border-b-#F8F8F8 h-full"> <div class="flex items-center border-b-1px border-b-solid border-b-#F8F8F8 h-full">