Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 83 additions & 94 deletions src/components/editor/draggable-block-plugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,8 @@ import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext
import {
$getNearestNodeFromDOMNode,
$getNodeByKey,
COMMAND_PRIORITY_HIGH,
DRAGOVER_COMMAND,
DROP_COMMAND,
type LexicalEditor,
} from "lexical";
import { mergeRegister } from "@lexical/utils";
import { GripVertical } from "lucide-react";

const DRAG_DATA_FORMAT = "application/x-memo-drag-block";
Expand Down Expand Up @@ -99,9 +95,12 @@ function getBlockElement(
}
}

// Verify the mouse is within the editor bounds horizontally
// For hover (non-drag), verify the mouse is within the editor bounds
// horizontally. During drag the cursor may be over the handle/padding area
// to the left of the editor — that's valid.
if (
blockElem &&
!useUnboundedSearch &&
(event.clientX < editorBounds.left - 50 ||
event.clientX > editorBounds.right + 50)
) {
Expand Down Expand Up @@ -272,96 +271,86 @@ export function DraggableBlockPlugin({
}
}, []);

// Register drag-over and drop commands on the Lexical editor
// Listen for dragover/drop on the anchorElem (the full-width container
// including the left padding where the drag handle lives). Lexical's
// DRAGOVER_COMMAND / DROP_COMMAND only fire on the contentEditable, so
// dragging straight down from the handle would miss the editor entirely.
const handleDragOver = useCallback(
(event: DragEvent) => {
if (!event.dataTransfer?.types.includes(DRAG_DATA_FORMAT)) return;

event.preventDefault();
event.dataTransfer.dropEffect = "move";

const blockElem = getBlockElement(anchorElem, editor, event, true);
if (!blockElem || !dropIndicatorRef.current) return;

const blockRect = blockElem.getBoundingClientRect();
const isBelow = event.clientY > blockRect.top + blockRect.height / 2;

setDropIndicatorPosition(
dropIndicatorRef.current,
blockElem,
anchorElem,
isBelow
);
},
[anchorElem, editor]
);

const handleDrop = useCallback(
(event: DragEvent) => {
if (!event.dataTransfer?.types.includes(DRAG_DATA_FORMAT)) return;

event.preventDefault();

const nodeKey = event.dataTransfer.getData(DRAG_DATA_FORMAT);
if (!nodeKey) return;

const blockElem = getBlockElement(anchorElem, editor, event, true);
if (!blockElem) return;

const blockRect = blockElem.getBoundingClientRect();
const isBelow = event.clientY > blockRect.top + blockRect.height / 2;

editor.update(() => {
const draggedNode = $getNodeByKey(nodeKey);
if (!draggedNode) return;

const targetNode = $getNearestNodeFromDOMNode(blockElem);
if (!targetNode) return;

// Don't drop on self
if (draggedNode.getKey() === targetNode.getKey()) return;

// Remove from current position
draggedNode.remove();

// Insert at new position
if (isBelow) {
targetNode.insertAfter(draggedNode);
} else {
targetNode.insertBefore(draggedNode);
}
});

if (dropIndicatorRef.current) {
dropIndicatorRef.current.style.opacity = "0";
}

isDraggingRef.current = false;
},
[anchorElem, editor]
);

useEffect(() => {
return mergeRegister(
editor.registerCommand(
DRAGOVER_COMMAND,
(event: DragEvent) => {
if (!event.dataTransfer?.types.includes(DRAG_DATA_FORMAT)) {
return false;
}

event.preventDefault();

const blockElem = getBlockElement(
anchorElem,
editor,
event,
true
);
if (!blockElem || !dropIndicatorRef.current) return true;

const blockRect = blockElem.getBoundingClientRect();
const isBelow = event.clientY > blockRect.top + blockRect.height / 2;

setDropIndicatorPosition(
dropIndicatorRef.current,
blockElem,
anchorElem,
isBelow
);

event.dataTransfer.dropEffect = "move";
return true;
},
COMMAND_PRIORITY_HIGH
),
editor.registerCommand(
DROP_COMMAND,
(event: DragEvent) => {
if (!event.dataTransfer?.types.includes(DRAG_DATA_FORMAT)) {
return false;
}

event.preventDefault();

const nodeKey = event.dataTransfer.getData(DRAG_DATA_FORMAT);
if (!nodeKey) return true;

const blockElem = getBlockElement(
anchorElem,
editor,
event,
true
);
if (!blockElem) return true;

const blockRect = blockElem.getBoundingClientRect();
const isBelow = event.clientY > blockRect.top + blockRect.height / 2;

editor.update(() => {
const draggedNode = $getNodeByKey(nodeKey);
if (!draggedNode) return;

const targetNode = $getNearestNodeFromDOMNode(blockElem);
if (!targetNode) return;

// Don't drop on self
if (draggedNode.getKey() === targetNode.getKey()) return;

// Remove from current position
draggedNode.remove();

// Insert at new position
if (isBelow) {
targetNode.insertAfter(draggedNode);
} else {
targetNode.insertBefore(draggedNode);
}
});

if (dropIndicatorRef.current) {
dropIndicatorRef.current.style.opacity = "0";
}

isDraggingRef.current = false;
return true;
},
COMMAND_PRIORITY_HIGH
)
);
}, [anchorElem, editor]);
anchorElem.addEventListener("dragover", handleDragOver);
anchorElem.addEventListener("drop", handleDrop);
return () => {
anchorElem.removeEventListener("dragover", handleDragOver);
anchorElem.removeEventListener("drop", handleDrop);
};
}, [anchorElem, handleDragOver, handleDrop]);

return createPortal(
<>
Expand Down
Loading