diff --git a/src/components/editor/CustomEditor.vue b/src/components/editor/CustomEditor.vue index 9fbab75..d32f597 100644 --- a/src/components/editor/CustomEditor.vue +++ b/src/components/editor/CustomEditor.vue @@ -174,6 +174,23 @@ const showMentionList = () => { 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列表 const hideMentionList = () => { showMention.value = false @@ -193,66 +210,74 @@ const updateMentionPosition = (range) => { } // 插入mention -const insertMention = (member) => { - console.log('插入mention',member) - const selection = window.getSelection() - if (!selection.rangeCount) return - - const range = selection.getRangeAt(0) - const textNode = range.startContainer - const offset = range.startOffset - - // 找到@符号的位置 - const textContent = textNode.textContent || '' - const atIndex = textContent.lastIndexOf('@', offset - 1) - - // 创建mention元素 - const mentionSpan = document.createElement('span') - mentionSpan.className = 'mention' - mentionSpan.setAttribute('data-user-id',String(member.id)) - mentionSpan.textContent = `@${member.value || member.nickname}` - mentionSpan.contentEditable = 'false' - - if (atIndex !== -1) { - // 如果找到@符号,替换文本 - const beforeText = textContent.substring(0, atIndex) - const afterText = textContent.substring(offset) - - // 创建新的文本节点 - const beforeNode = document.createTextNode(beforeText) - const afterNode = document.createTextNode(' ' + afterText) - - // 替换内容 - const parent = textNode.parentNode - parent.insertBefore(beforeNode, textNode) - parent.insertBefore(mentionSpan, textNode) - parent.insertBefore(afterNode, textNode) - parent.removeChild(textNode) +const insertMention = (member, clonedRange) => { + console.log('插入mention', member); + const selection = window.getSelection(); + if (!clonedRange || !selection) return; + + const range = clonedRange; // 使用传入的克隆 range + + const textNode = range.startContainer; + const offset = range.startOffset; + const textContent = textNode.nodeType === Node.TEXT_NODE ? textNode.textContent || '' : ''; + // @符号的查找逻辑仅当光标在文本节点内且不在开头时才有意义 + const atIndex = (textNode.nodeType === Node.TEXT_NODE && offset > 0) ? textContent.lastIndexOf('@', offset - 1) : -1; + + const mentionSpan = document.createElement('span'); + mentionSpan.className = 'mention'; + mentionSpan.setAttribute('data-user-id', String(member.id)); + mentionSpan.textContent = `@${member.value || member.nickname}`; + mentionSpan.contentEditable = 'false'; + + if (atIndex !== -1 && textNode.nodeType === Node.TEXT_NODE) { + const parent = textNode.parentNode; + if (!parent) return; // Sanity check + + const textBeforeAt = textContent.substring(0, atIndex); + const textAfterCursor = textContent.substring(offset); + + const beforeNode = document.createTextNode(textBeforeAt); + const spaceAfterMentionNode = document.createTextNode(' '); // Ensure space after mention + const afterNode = document.createTextNode(textAfterCursor); + + parent.insertBefore(beforeNode, textNode); + parent.insertBefore(mentionSpan, textNode); + parent.insertBefore(spaceAfterMentionNode, textNode); + parent.insertBefore(afterNode, textNode); + parent.removeChild(textNode); + + range.setStartAfter(spaceAfterMentionNode); + range.collapse(true); } else { - // 如果没有找到@符号,直接在光标位置插入 - 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) + // 如果没有找到@符号,或者光标不在合适的文本节点内,直接在当前光标位置插入 + if (!range.collapsed) { + range.deleteContents(); + } + + range.insertNode(mentionSpan); + + const spaceNode = document.createTextNode(' '); + // 正确地将 range 移动到 mentionSpan 之后再插入空格 + const tempRangeForSpace = range.cloneRange(); // 使用临时 range 避免干扰主 range + tempRangeForSpace.setStartAfter(mentionSpan); + tempRangeForSpace.collapse(true); + tempRangeForSpace.insertNode(spaceNode); + + // 将主 range 移动到新插入的空格之后 + range.setStartAfter(spaceNode); + range.collapse(true); } - - // 触发输入事件以更新编辑器内容 - handleInput({ target: editorRef.value }) - - // 隐藏mention列表 - hideMentionList() -} + + selection.removeAllRanges(); + selection.addRange(range); + + editorRef.value?.focus(); // 确保编辑器在操作后仍有焦点 + + nextTick(() => { + handleInput({ target: editorRef.value }); + hideMentionList(); + }); +}; // 处理粘贴事件 const handlePaste = (event) => { @@ -350,12 +375,15 @@ const handleKeydown = (event) => { break case 'Enter': case 'Tab': - event.preventDefault() - const selectedMember = mentionList.value[selectedMentionIndex.value] + event.preventDefault(); + const selectedMember = mentionList.value[selectedMentionIndex.value]; if (selectedMember) { - insertMention(selectedMember) + const selection = window.getSelection(); + if (selection && selection.rangeCount > 0) { + insertMention(selectedMember, selection.getRangeAt(0).cloneRange()); + } } - break + break; case 'Escape': hideMentionList() break @@ -398,6 +426,25 @@ const handleKeydown = (event) => { } 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) { // 如果光标在元素节点中,检查前一个子节点 @@ -930,27 +977,44 @@ const insertImageEmoji = (imgSrc, altText) => { } // 事件监听 -const onSubscribeMention = (data) => { - // 确保编辑器获得焦点 - editorRef.value?.focus() - - // 如果编辑器为空或者光标不在编辑器内,将光标移动到编辑器末尾 - const selection = window.getSelection() - if (!selection.rangeCount || !editorRef.value.contains(selection.anchorNode)) { - const range = document.createRange() - if (editorRef.value.lastChild) { - range.setStartAfter(editorRef.value.lastChild) +const onSubscribeMention = async (data) => { + const editorNode = editorRef.value; + if (!editorNode) return; + + editorNode.focus(); + await nextTick(); // 确保焦点已设置 + + let selection = window.getSelection(); + if (!selection || selection.rangeCount === 0) { + const range = document.createRange(); + if (editorNode.lastChild) { + range.setStartAfter(editorNode.lastChild); } else { - range.setStart(editorRef.value, 0) + range.setStart(editorNode, 0); } - range.collapse(true) - selection.removeAllRanges() - selection.addRange(range) + range.collapse(true); + if (selection) selection.removeAllRanges(); + 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(); } - - // 插入@提及 - insertMention(data) -} + + if (selection && selection.rangeCount > 0) { + insertMention(data, selection.getRangeAt(0).cloneRange()); + } +}; const onSubscribeQuote = (data) => { // 保存引用数据,但不保存到草稿中 @@ -1443,7 +1507,7 @@ const handleEditorClick = (event) => {

{{ nav.title }}

- +