Drag and Drop Block Issue with VanillaJS in SvelteKit
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:
- Initialize BlockNoteEditor using the VanillaJS API within a SvelteKit component.
- Create a custom drag button (::) that triggers the block drag behavior via blockDragStart and blockDragEnd.
- 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
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;
}
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!
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.