How to Add a Visual Indicator to a Node in Tiptap During Drag-and-Drop Operations?

  1. I am working on implementing a drag-and-drop feature in a Tiptap editor, similar to the WordPress block editor. My goal is to show a border underline beneath the node where a draggable component (a custom toolbar’s drag handle) is currently hovered, giving users a clear indication of where the node will be placed when they release the drag handle.

    I have successfully implemented the basic drag-and-drop functionality where I can move content around. However, I am struggling to add the visual feedback during the drag operation. Specifically, I want to dynamically apply a CSS class to underline the node that is directly beneath the cursor or drag handle.

    Here is the relevant part of my React component that handles the drag events:

    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;
          },
        };
      } 
    });
    

Problem:
While the drag operations are detected, the decorations are not being applied as expected. The underline does not appear beneath the hovered node.

Attempts:

  • I confirmed the positions calculated are correct and that the updateDecoration function is being called with the correct parameters.
  • I verified that the DecorationSet.create method is used properly to apply the decorations.
  • I ensured that the CSS class for .hovered is correctly defined to show an border bottom underline.

Can anyone spot what might be going wrong, or suggest an alternative approach to achieve the desired visual feedback during the drag-and-drop operations in Tiptap?