TypeCellOS / BlockNote

A React Rich Text Editor that's block-based (Notion style) and extensible. Built on top of Prosemirror and Tiptap.
https://www.blocknotejs.org/
Mozilla Public License 2.0
6.78k stars 475 forks source link

Drag and Drop Block Issue with VanillaJS in SvelteKit #1051

Open ankified opened 2 months ago

ankified commented 2 months ago

Description:

I’m using the VanillaJS version of BlockNote within a SvelteKit project. While the editor works mostly as expected, I’m encountering an issue with drag-and-drop behavior.

Problem:

When dragging a block (using a custom drag button I created), the block's content is also being displayed underneath the editor, as though it’s being dragged separately in the DOM. This additional content persists below the editor during the drag action and doesn't behave as expected.

Steps to Reproduce:

  1. Initialize BlockNoteEditor using the VanillaJS API within a SvelteKit component.
  2. Create a custom drag button (::) that triggers the block drag behavior via blockDragStart and blockDragEnd.
  3. Type something and drag a block using this custom button.

Expected Behavior:

Dragging a block should only move the block within the editor, without creating an additional visual artifact (the block’s content) under the editor in the DOM.

Current Behavior:

The block's content is displayed beneath the editor during the drag event, creating a visual artifact that shouldn't be there.

Code Example:

<script lang="ts">
  import { onMount, onDestroy } from 'svelte';
  import { BlockNoteEditor } from '@blocknote/core';
  let editor: any; // Variable to hold the editor instance
  let buttonContainer: HTMLElement | null = null; // Variable to hold the container for the custom buttons

  // Function to create a button with the specified text and an optional click handler
  function createButton(text: string, onClick?: () => void) {
    const btn = document.createElement('button');
    btn.innerText = text; // Set button text
    btn.style.margin = '1px'; // Style the button

    // If an onClick handler is provided, attach it to the button
    if (onClick) {
      btn.addEventListener('click', (e) => {
        onClick();
        e.preventDefault(); // Prevent default action (like following a link)
      });
    }

    return btn; // Return the created button
  }

  // Lifecycle hook: Run when the component is mounted
  onMount(() => {
    const rootElement = document.getElementById('root'); // Get the root element for the editor

    // If the root element is not found, log an error and stop
    if (!rootElement) {
      console.error('Root element not found!');
      return;
    }

    // Initialize and mount the BlockNote editor on the root element
    editor = BlockNoteEditor.create();
    editor.mount(rootElement);
    editor.forEachBlock((block: any) => {
      console.log(block); // Log each block for debugging purposes
    });

    // Listen for side menu updates to show or hide the button container
    editor.sideMenu.onUpdate((sideMenuState: any) => {
      // If the button container doesn't exist yet, create it
      if (!buttonContainer) {
        buttonContainer = document.createElement('div');
        buttonContainer.style.background = 'rgba(128, 128, 128, 0)'; // Make it transparent
        buttonContainer.style.position = 'absolute'; // Position it absolutely
        buttonContainer.style.padding = '10px'; // Add some padding
        buttonContainer.style.zIndex = '1000'; // Make sure it's above other elements

        // Create the "add block" button and append it to the button container
        const addBtn = createButton('+', () => {
          console.log('Add button clicked');
          editor.sideMenu.addBlock(); // Add a block when the button is clicked
        });
        buttonContainer.appendChild(addBtn);
        console.log('Add button created and appended:', addBtn);

        // Create the "drag block" button and append it to the button container
        const dragBtn = createButton('::', () => {});
        dragBtn.addEventListener('dragstart', (e) => editor.sideMenu.blockDragStart(e)); // Handle block dragging
        dragBtn.addEventListener('dragend', (e) => editor.sideMenu.blockDragEnd(e)); // Handle block drag end
        dragBtn.draggable = true; // Make the button draggable
        buttonContainer.style.display = "none"; // Initially hide the button container
        buttonContainer!.appendChild(dragBtn);
        console.log('Drag button created and appended:', dragBtn);

        // Insert the button container before the root element
        rootElement.insertAdjacentElement('beforebegin', buttonContainer);
      }

      // Show or hide the button container based on the sideMenuState
      if (sideMenuState.show === true) {
        buttonContainer.style.display = "block"; // Show the container
        buttonContainer.style.top = (sideMenuState.referencePos.top - 20) + "px"; // Set its top position
        buttonContainer.style.left = sideMenuState.referencePos.x - buttonContainer.offsetWidth + "px"; // Set its left position
      } else {
        buttonContainer!.style.display = 'none'; // Hide the container
      }
    });
  });

  // Lifecycle hook: Run when the component is destroyed
  onDestroy(() => {
    if (editor) {
      editor.destroy(); // Clean up the editor instance
    }
    if (buttonContainer) {
      buttonContainer.remove(); // Remove the button container from the DOM
    }
  });
</script>

<!-- Editor container -->
<div id="root" style="min-height: 300px; border: 1px solid #ccc; margin: 50px; padding: 10px;"></div>

StackBlitz example:

https://stackblitz.com/edit/sveltejs-kit-template-default-1rrnqy?file=src%2Froutes%2F%2Bpage.svelte

matthewlipski commented 2 months ago

Ah, I think this is actually a CSS issue. Try adding this:

.bn-drag-preview {
  position: absolute;
  top: 0;
  left: 0;
  padding: 10px;
  opacity: 0.001;
}
ankified commented 2 months ago

Hi @matthewlipski,

Thank you so much for providing the solution to this issue, I really appreciate it!

As a collaborator on the repository, I wanted to ask if you happen to know whether it’s possible to use ProseMirror or TipTap plugins with the VanillaJS implementation of BlockNote. Specifically, I’m looking into plugins like the TipTap comment extension (https://github.com/sereneinserenade/tiptap-comment-extension), the TipTap plugin that allows users to add Svelte components to the editor's content (https://github.com/sibiraj-s/svelte-tiptap), and the ProseMirror plugin for toggling between Markdown and WYSIWYM views (https://prosemirror.net/examples/markdown/).

If this isn’t the appropriate place to ask these kinds of questions, would it be better to open a new issue dedicated to this topic?

Thanks again for your help!

matthewlipski commented 2 months ago

While I can't say for sure that things will work out of the box, you can add TipTap extensions to BlockNote like so:

const editor = BlockNoteEditor.create({
  _tiptapOptions: {
    extensions: [
      // Add extensions here
    ]
  }
})

And likewise with ProseMirror plugins, you just have to wrap them in a TipTap extension first (see https://tiptap.dev/docs/editor/extensions/custom-extensions/extend-existing#prosemirror-plugins-advanced).

Again I can't say for sure whether the ones you listed will work right away, will require some workarounds, or won't work at all, so just give them a shot and feel free to continue this thread if you run into issues.