feat(editor): 添加鼠标点击选择mention功能并优化插入逻辑
- 新增handleMentionSelectByMouse函数处理鼠标点击选择mention - 重构insertMention函数,支持传入range参数并优化插入逻辑 - 修复mention列表点击事件,防止默认行为导致的问题 - 优化onSubscribeMention函数,确保焦点和选区正确处理
This commit is contained in:
parent
bdf07155c8
commit
d4e52152ef
@ -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">
|
||||||
|
Loading…
Reference in New Issue
Block a user