Tiptap拖拽操作中节点视觉指示器添加失败问题排查
Tiptap拖拽视觉反馈问题排查与解决
问题描述
我正在为Tiptap编辑器实现类似WordPress区块编辑器的拖拽功能,目标是在拖拽手柄悬停的节点下方显示下划线边框,明确释放拖拽时的放置位置。目前基础拖拽移动功能已实现,但无法添加拖拽过程中的视觉反馈——无法为光标/拖拽手柄下方的节点动态应用CSS类。
相关代码
import React, { createContext, useContext, useState, useEffect } from 'react'; import Draggable from 'react-draggable'; import { Extension } from "@tiptap/core"; import { Decoration, DecorationSet } from '@tiptap/pm/view'; import { Plugin, PluginKey } from '@tiptap/pm/state'; export const DragHandle = ({ editor }) => { const moveNode = (editor, fromPos, toPos) => { const { tr } = editor.view.state; const node = editor.view.state.doc.nodeAt(fromPos); if (node) { tr.delete(fromPos, fromPos + node.nodeSize); // Delete the node from its current position tr.insert(toPos, node); // Insert the node at the new position editor.view.dispatch(tr); // Dispatch the transaction to update the editor state } }; const [currentDecoration, setCurrentDecoration] = useState(null); const updateDecoration = (pos) => { console.log("Updating decoration at pos:", pos); const { doc } = editor.state; const resolvedPos = doc.resolve(pos); console.log("Resolved position:", resolvedPos); const node = resolvedPos.nodeAfter; if (node) { const startPos = resolvedPos.before(); // Get the position before the node starts const endPos = startPos + node.nodeSize; console.log("Node start:", startPos, "Node end:", endPos); const decorations = DecorationSet.create(doc, [ Decoration.node(startPos, endPos, { class: 'hovered' }) ]); console.log("Decorations to apply:", decorations); editor.commands.updateHoverFocusDecorations(decorations); setCurrentDecoration(decorations); } else if (currentDecoration) { console.log("Clearing decorations because no node is found at the position"); editor.commands.updateHoverFocusDecorations(DecorationSet.empty); setCurrentDecoration(null); } }; const handleDragStart = (event, data) => { const pos = editor.view.posAtCoords({ left: data.x, top: data.y }); if (pos) { updateDecoration(pos.pos); } }; const handleDrag = (event, data) => { const pos = editor.view.posAtCoords({ left: data.x, top: data.y }); if (pos) { console.log("Drag at position:", pos.pos); updateDecoration(pos.pos); } }; const handleDragStop = () => { if (currentDecoration) { editor.commandons.updateHoverFocusDecorations(DecorationSet.empty); setCurrentDecoration(null); } }; // const handleDragStop = (event, data) => { // const { clientX, clientY } = event; // const dropPosition = editor.view.posAtCoords({ left: clientX, top: clientY }); // console.log('HJHJ', dropPosition) // if (dropPosition) { // // moveNode(editor, please, dropPosition.pos) // Please is the position of the node that was dragged start // } // console.log('Drag stopped at:', data.x, data.y); // if (!editor) return; // editor.commands.updateHoverFocusDecorations(DecorationSet.empty); // }; return ( <Draggable onStart={handleDragStart} onDrag={handleDrag} onStop={handleDragStop} > <div className='blah' style={{ zIndex: 999 }}> <div className='drag-handle' /> </div> </Draggable> ) } export const DragHandleExtension = Extension.create({ name: 'Hovered', addOptions() { return { className: 'hovered', // The CSS class to apply }; }, addProseMirrorPlugins() { return [ new Plugin({ key: new PluginKey('Hovered'), state: { init: () => DecorationSet.empty, apply: (tr, oldState) => { const decorations = tr.getMeta('Hovered'); if (decorations) { return decorations; } return oldState.map(tr.mapping, tr.doc); // Map old decorations to the new document }, }, props: { decorations(state) { return this.getState(state); } } }) ]; }, addCommands() { return { updateHoverFocusDecorations: (decorations) => ({ tr, dispatch }) => { dispatch(tr.setMeta('Hovered', decorations)); return true; }, }; } });
已尝试操作
- 确认计算的位置正确,
updateDecoration函数参数无误; - 验证
DecorationSet.create方法使用正确; - 确保
.hoveredCSS类已正确定义以显示下划线边框。
问题排查与解决方案
1. 拼写错误导致命令失效
在handleDragStop函数中,editor.commandons是拼写错误,正确应为editor.commands,这会导致拖拽结束后无法清除装饰,也可能影响装饰的更新逻辑。
修正后的handleDragStop:
const handleDragStop = () => { if (currentDecoration) { editor.commands.updateHoverFocusDecorations(DecorationSet.empty); setCurrentDecoration(null); } };
2. 节点位置计算错误
resolvedPos.before()获取的是当前位置所在父节点的起始位置,而非目标节点的起始位置。当resolvedPos.nodeAfter存在时,目标节点的起始位置就是resolvedPos.pos,结束位置为resolvedPos.pos + node.nodeSize。
修正后的updateDecoration函数:
const updateDecoration = (pos) => { console.log("Updating decoration at pos:", pos); const { doc } = editor.state; const resolvedPos = doc.resolve(pos); console.log("Resolved position:", resolvedPos); const node = resolvedPos.nodeAfter; if (node) { const startPos = resolvedPos.pos; // 目标节点的起始位置 const endPos = startPos + node.nodeSize; console.log("Node start:", startPos, "Node end:", endPos); const decorations = DecorationSet.create(doc, [ Decoration.node(startPos, endPos, { class: 'hovered' }) ]); console.log("Decorations to apply:", decorations); editor.commands.updateHoverFocusDecorations(decorations); setCurrentDecoration(decorations); } else if (currentDecoration) { console.log("Clearing decorations because no node is found at the position"); editor.commands.updateHoverFocusDecorations(DecorationSet.empty); setCurrentDecoration(null); } };
3. 拖拽坐标不准确
react-draggable的data.x/data.y是拖拽元素的偏移量,不是鼠标在页面的绝对坐标,应该使用event.clientX和event.clientY来获取鼠标的实际位置,这样posAtCoords才能正确计算对应的文档位置。
修正后的拖拽事件处理函数:
const handleDragStart = (event) => { const pos = editor.view.posAtCoords({ left: event.clientX, top: event.clientY }); if (pos) { updateDecoration(pos.pos); } }; const handleDrag = (event) => { const pos = editor.view.posAtCoords({ left: event.clientX, top: event.clientY }); if (pos) { console.log("Drag at position:", pos.pos); updateDecoration(pos.pos); } };
替代方案:直接DOM操作
如果ProseMirror装饰方案仍有问题,可以尝试直接通过DOM操作添加视觉反馈:
const updateVisualFeedback = (event) => { // 先清除之前的hover类 document.querySelectorAll('.hovered').forEach(el => el.classList.remove('hovered')); // 获取鼠标下方的元素 const target = document.elementFromPoint(event.clientX, event.clientY); // 找到对应的Tiptap节点元素(根据你的节点结构调整选择器) const nodeEl = target.closest('[data-node-type]'); // 假设节点有data-node-type属性 if (nodeEl) { nodeEl.classList.add('hovered'); } }; // 在拖拽事件中调用 const handleDrag = (event) => { updateVisualFeedback(event); }; const handleDragStop = () => { document.querySelectorAll('.hovered').forEach(el => el.classList.remove('hovered')); };
内容的提问来源于stack exchange,提问作者Txie B
相关产品推荐
相关产品推荐

