fix: 块编辑器关闭链接弹窗后被选中的文本背景色还在
This commit is contained in:
parent
f1c3ba8d16
commit
e6df606a6f
@ -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;
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user