You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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方法使用正确;
  • 确保.hovered CSS类已正确定义以显示下划线边框。

问题排查与解决方案

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 08:52:03