From d4e52152efb509bfd4847dd3cdb5c60b1a6bfd92 Mon Sep 17 00:00:00 2001 From: Phoenix <64720302+Concur-max@users.noreply.github.com> Date: Tue, 10 Jun 2025 09:45:10 +0800 Subject: [PATCH] =?UTF-8?q?feat(editor):=20=E6=B7=BB=E5=8A=A0=E9=BC=A0?= =?UTF-8?q?=E6=A0=87=E7=82=B9=E5=87=BB=E9=80=89=E6=8B=A9mention=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E5=B9=B6=E4=BC=98=E5=8C=96=E6=8F=92=E5=85=A5=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增handleMentionSelectByMouse函数处理鼠标点击选择mention - 重构insertMention函数,支持传入range参数并优化插入逻辑 - 修复mention列表点击事件,防止默认行为导致的问题 - 优化onSubscribeMention函数,确保焦点和选区正确处理 --- src/components/editor/CustomEditor.vue | 228 ++++++++++++++++--------- 1 file changed, 146 insertions(+), 82 deletions(-) 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 }}

- +