fix: 块编辑器关闭链接弹窗后被选中的文本背景色还在

This commit is contained in:
jingrow 2026-06-03 03:36:56 +08:00
parent f1c3ba8d16
commit e6df606a6f
2 changed files with 85 additions and 16 deletions

View File

@ -125,11 +125,9 @@ const {
// Link Panel
const linkPanelRef = ref<InstanceType<typeof LinkPanel> | null>(null)
const linkPanelAnchor = ref<DOMRect | null>(null)
function openLinkPanel(rect?: DOMRect) {
linkPanelAnchor.value = rect || null
linkPanelRef.value?.open()
linkPanelRef.value?.open(rect)
}
// Slash menu events
@ -204,7 +202,6 @@ onBeforeUnmount(() => {
ref="linkPanelRef"
:editor="editorRef"
:t="t"
:anchor-rect="linkPanelAnchor"
/>
<!-- Bubble Menu -->
@ -445,6 +442,12 @@ onBeforeUnmount(() => {
margin-bottom: 4px;
}
/* 弹窗打开时选中文字高亮ProseMirror Decoration不受浏览器焦点影响 */
.blockeditor-fake-selection {
background: #cce4ff;
border-radius: 2px;
}
/* ── Link Popover ── */
.blockeditor-link-popover {
display: flex;

View File

@ -3,16 +3,21 @@
* LinkPanel.vue 链接插入/编辑/移除面板
*
* 定位在工具栏下方紧挨着工具栏按钮弹出设计参考 TipTap Link Popover
* 父组件通过 ref.open() 调用打开
* 父组件通过 ref.open(anchorRect?) 调用打开
*
* 选区高亮使用 ProseMirror Decoration已注册到编辑器不受浏览器焦点影响
* 按钮/输入框使用 @mousedown.stop 防止 blur 冒泡
* 关闭时恢复编辑器焦点和选区
*/
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'
import type { Editor } from '@tiptap/core'
import { Plugin, PluginKey } from '@tiptap/pm/state'
import { Decoration, DecorationSet } from '@tiptap/pm/view'
import { renderIcon } from '../toolbar/icons'
const props = defineProps<{
editor: Editor | null
t: (key: string) => string
anchorRect?: DOMRect | null
}>()
const show = ref(false)
@ -22,19 +27,35 @@ const popoverEl = ref<HTMLElement | null>(null)
const popoverLeft = ref(0)
const popoverTop = ref(0)
function open() {
const hlKey = new PluginKey('blockeditor-link-selection')
let savedFrom = 0
let savedTo = 0
// props.decorations plugin state
let highlightRange: { from: number; to: number } | null = null
//
function open(anchorRect?: DOMRect) {
show.value = true
//
if (props.anchorRect && props.editor) {
if (props.editor) {
const { from, to } = props.editor.state.selection
savedFrom = from
savedTo = to
highlightRange = { from, to }
forceUpdate()
}
//
if (anchorRect && props.editor) {
const container = props.editor.options.element.closest('.blockeditor-container') as HTMLElement | null
if (container) {
const cr = container.getBoundingClientRect()
popoverLeft.value = props.anchorRect.left - cr.left
popoverTop.value = props.anchorRect.bottom - cr.top + 2
popoverLeft.value = anchorRect.left - cr.left
popoverTop.value = anchorRect.bottom - cr.top + 2
}
} else if (props.editor) {
// anchorRect /
const container = props.editor.options.element.closest('.blockeditor-container') as HTMLElement | null
if (container) {
const cr = container.getBoundingClientRect()
@ -50,7 +71,6 @@ function open() {
const attrs = props.editor.getAttributes('link')
url.value = attrs?.href || ''
newWindow.value = attrs?.target === '_blank'
props.editor.chain().focus().run()
} else {
url.value = ''
newWindow.value = false
@ -79,20 +99,62 @@ function removeLink() {
function close() {
show.value = false
highlightRange = null
forceUpdate()
if (props.editor) {
props.editor.chain().focus().setTextSelection({ from: savedFrom, to: savedTo }).run()
}
}
//
function onDocumentMousedown(e: MouseEvent) {
if (!show.value) return
if (popoverEl.value && !popoverEl.value.contains(e.target as Node)) {
show.value = false
close()
}
}
onMounted(() => document.addEventListener('mousedown', onDocumentMousedown))
onBeforeUnmount(() => document.removeEventListener('mousedown', onDocumentMousedown))
onMounted(() => {
document.addEventListener('mousedown', onDocumentMousedown)
if (props.editor && !props.editor.state.plugins.find(p => (p as any).key === hlKey)) {
props.editor.registerPlugin(new DecorHighlightsPlugin())
}
})
onBeforeUnmount(() => {
document.removeEventListener('mousedown', onDocumentMousedown)
if (props.editor) {
props.editor.unregisterPlugin('blockeditor-link-selection')
}
})
defineExpose({ open })
// ProseMirror Decoration
// props.decorations highlightRange plugin state apply
function DecorHighlightsPlugin() {
return new Plugin({
key: hlKey,
props: {
decorations(state) {
if (!highlightRange || highlightRange.from === highlightRange.to) {
return DecorationSet.empty
}
return DecorationSet.create(state.doc, [
Decoration.inline(highlightRange.from, highlightRange.to, {
class: 'blockeditor-fake-selection',
}),
])
},
},
})
}
function forceUpdate() {
if (!props.editor) return
props.editor.view.dispatch(props.editor.state.tr.setMeta('addToHistory', false))
}
</script>
<template>
@ -114,6 +176,7 @@ defineExpose({ open })
class="blockeditor-link-input"
@keydown.enter="setLink"
@keydown.esc="close"
@mousedown.stop
/>
<span class="blockeditor-link-actions">
<button
@ -122,6 +185,7 @@ defineExpose({ open })
:title="t('Open in new window')"
v-html="renderIcon('external_link')"
@click="newWindow = !newWindow"
@mousedown.stop
/>
<button
class="blockeditor-link-btn"
@ -129,6 +193,7 @@ defineExpose({ open })
v-html="renderIcon('link')"
:disabled="!url.trim()"
@click="setLink"
@mousedown.stop
/>
<button
v-if="url"
@ -136,6 +201,7 @@ defineExpose({ open })
:title="t('Remove link')"
v-html="renderIcon('trash')"
@click="removeLink"
@mousedown.stop
/>
</span>
</div>