React drag and drop plugin for Plate rich-text editor enabling block rearrangement and file drops
—
Helper functions for calculating drop directions, handling hover states, and determining valid drop operations. These utilities provide the low-level calculations and state management needed for accurate drag-and-drop behavior.
Functions for determining and managing drop directions based on mouse position and element geometry.
Calculates the hover direction based on mouse position relative to an element's bounds, supporting both vertical and horizontal orientations.
/**
* Calculates hover direction from mouse coordinates and element bounds
* Determines which edge of an element the mouse is closest to
* @param options - Configuration for direction calculation
* @returns Direction string ('top', 'bottom', 'left', 'right', or empty)
*/
export function getHoverDirection(options: GetHoverDirectionOptions): string;
export interface GetHoverDirectionOptions {
/** Current mouse coordinates */
clientOffset: { x: number; y: number };
/** Element's bounding rectangle */
hoveredClientRect: DOMRect;
/** Drag orientation */
orientation?: 'horizontal' | 'vertical';
/** Threshold for edge detection (0-1) */
threshold?: number;
}Usage Examples:
import { getHoverDirection } from "@udecode/plate-dnd";
// In a drag hover handler
function handleDragHover(monitor: DropTargetMonitor, elementRef: React.RefObject<HTMLElement>) {
const clientOffset = monitor.getClientOffset();
const element = elementRef.current;
if (!clientOffset || !element) return;
const direction = getHoverDirection({
clientOffset,
hoveredClientRect: element.getBoundingClientRect(),
orientation: 'vertical',
threshold: 0.25
});
console.log('Hover direction:', direction);
return direction;
}
// Custom threshold for different sensitivity
function getSensitiveHoverDirection(
clientOffset: { x: number; y: number },
element: HTMLElement
) {
return getHoverDirection({
clientOffset,
hoveredClientRect: element.getBoundingClientRect(),
orientation: 'vertical',
threshold: 0.1 // More sensitive (10% of element height)
});
}
// Horizontal orientation example
function getHorizontalDirection(
clientOffset: { x: number; y: number },
element: HTMLElement
) {
return getHoverDirection({
clientOffset,
hoveredClientRect: element.getBoundingClientRect(),
orientation: 'horizontal',
threshold: 0.3
});
}
// In a custom drop zone component
function CustomDropZone({ children }) {
const elementRef = useRef<HTMLDivElement>(null);
const [hoverDirection, setHoverDirection] = useState<string>('');
const [{ isOver }, drop] = useDrop({
accept: 'block',
hover: (item, monitor) => {
const clientOffset = monitor.getClientOffset();
if (!clientOffset || !elementRef.current) return;
const direction = getHoverDirection({
clientOffset,
hoveredClientRect: elementRef.current.getBoundingClientRect(),
orientation: 'vertical'
});
setHoverDirection(direction);
},
collect: (monitor) => ({
isOver: monitor.isOver()
})
});
const combinedRef = useCallback((el: HTMLDivElement) => {
elementRef.current = el;
drop(el);
}, [drop]);
return (
<div
ref={combinedRef}
style={{
position: 'relative',
minHeight: '60px',
border: isOver ? '2px dashed #007acc' : '2px solid transparent'
}}
>
{/* Visual indicator based on hover direction */}
{isOver && hoverDirection && (
<div
style={{
position: 'absolute',
backgroundColor: '#007acc',
[hoverDirection]: '-2px',
...(hoverDirection === 'top' || hoverDirection === 'bottom'
? { left: 0, right: 0, height: '4px' }
: { top: 0, bottom: 0, width: '4px' }
)
}}
/>
)}
{children}
</div>
);
}Determines if a direction change has occurred and returns the new direction if different from the previous state.
/**
* Determines new drop direction based on previous state
* Returns new direction only if it differs from the previous direction
* @param previousDir - The previous direction state
* @param dir - The current direction
* @returns New direction or undefined if no change
*/
export function getNewDirection(
previousDir: string,
dir?: string
): DropLineDirection | undefined;
export type DropLineDirection = '' | 'bottom' | 'left' | 'right' | 'top';Usage Examples:
import { getNewDirection } from "@udecode/plate-dnd";
// State management for direction changes
function useDirectionState() {
const [currentDirection, setCurrentDirection] = useState<string>('');
const updateDirection = (newDir?: string) => {
const changedDirection = getNewDirection(currentDirection, newDir);
if (changedDirection !== undefined) {
setCurrentDirection(changedDirection);
console.log('Direction changed to:', changedDirection);
return true; // Direction changed
}
return false; // No change
};
return { currentDirection, updateDirection };
}
// In a hover handler with direction tracking
function SmartHoverHandler({ onDirectionChange }) {
const [lastDirection, setLastDirection] = useState<string>('');
const handleHover = (monitor: DropTargetMonitor, element: HTMLElement) => {
const clientOffset = monitor.getClientOffset();
if (!clientOffset) return;
const currentDir = getHoverDirection({
clientOffset,
hoveredClientRect: element.getBoundingClientRect(),
orientation: 'vertical'
});
const newDirection = getNewDirection(lastDirection, currentDir);
if (newDirection !== undefined) {
setLastDirection(newDirection);
onDirectionChange?.(newDirection);
}
};
return { handleHover };
}
// Debounced direction changes
function useDebouncedDirection(delay: number = 50) {
const [direction, setDirection] = useState<string>('');
const [debouncedDirection, setDebouncedDirection] = useState<string>('');
useEffect(() => {
const timer = setTimeout(() => {
const newDir = getNewDirection(debouncedDirection, direction);
if (newDir !== undefined) {
setDebouncedDirection(newDir);
}
}, delay);
return () => clearTimeout(timer);
}, [direction, debouncedDirection, delay]);
return { direction: debouncedDirection, setDirection };
}Functions for finding and filtering editor nodes based on specific criteria.
Finds all blocks in the editor that have an ID property, which is essential for drag-and-drop operations.
/**
* Get blocks with an id property
* Finds all editor blocks that have an ID, which are draggable
* @param editor - The editor instance
* @param options - Options for node searching
* @returns Array of node entries for blocks with IDs
*/
export function getBlocksWithId<E extends Editor>(
editor: E,
options: EditorNodesOptions<ValueOf<E>>
): NodeEntry<TElement>[];
export type EditorNodesOptions<T> = {
/** Location to search within */
at?: Location;
/** Function to match specific nodes */
match?: (node: T) => boolean;
/** Search mode */
mode?: 'all' | 'highest' | 'lowest';
/** Whether to include universal nodes */
universal?: boolean;
/** Whether to search in reverse order */
reverse?: boolean;
/** Whether to include void nodes */
voids?: boolean;
};Usage Examples:
import { getBlocksWithId } from "@udecode/plate-dnd";
// Get all draggable blocks in the editor
function getAllDraggableBlocks(editor: Editor) {
return getBlocksWithId(editor, { at: [] });
}
// Get draggable blocks in the current selection
function getDraggableBlocksInSelection(editor: Editor) {
if (!editor.selection) return [];
return getBlocksWithId(editor, {
at: editor.selection
});
}
// Get specific types of draggable blocks
function getDraggableBlocksByType(editor: Editor, blockType: string) {
return getBlocksWithId(editor, {
match: (node) => node.type === blockType,
at: []
});
}
// Find draggable blocks in a specific range
function getDraggableBlocksInRange(editor: Editor, from: Path, to: Path) {
return getBlocksWithId(editor, {
at: { anchor: { path: from, offset: 0 }, focus: { path: to, offset: 0 } }
});
}
// Count draggable blocks
function countDraggableBlocks(editor: Editor): number {
const blocks = getBlocksWithId(editor, { at: [] });
return blocks.length;
}
// Get draggable blocks with custom filtering
function getFilteredDraggableBlocks(
editor: Editor,
filter: (element: TElement) => boolean
) {
const allBlocks = getBlocksWithId(editor, { at: [] });
return allBlocks.filter(([element]) => filter(element));
}
// Usage in a component
function DraggableBlocksList() {
const editor = useEditorRef();
const [blocks, setBlocks] = useState<NodeEntry<TElement>[]>([]);
useEffect(() => {
const draggableBlocks = getBlocksWithId(editor, { at: [] });
setBlocks(draggableBlocks);
}, [editor]);
return (
<div>
<h3>Draggable Blocks ({blocks.length})</h3>
{blocks.map(([element, path]) => (
<div key={element.id as string}>
Block ID: {element.id} at path: {path.join('.')}
</div>
))}
</div>
);
}Combining utilities for complete drag-and-drop behavior:
import {
getHoverDirection,
getNewDirection,
getBlocksWithId
} from "@udecode/plate-dnd";
// Complete hover direction management
function useSmartHoverDirection(
elementRef: React.RefObject<HTMLElement>,
orientation: 'horizontal' | 'vertical' = 'vertical'
) {
const [direction, setDirection] = useState<string>('');
const updateDirection = useCallback((monitor: DropTargetMonitor) => {
const clientOffset = monitor.getClientOffset();
const element = elementRef.current;
if (!clientOffset || !element) return;
const currentDir = getHoverDirection({
clientOffset,
hoveredClientRect: element.getBoundingClientRect(),
orientation
});
const newDir = getNewDirection(direction, currentDir);
if (newDir !== undefined) {
setDirection(newDir);
}
}, [direction, elementRef, orientation]);
return { direction, updateDirection };
}
// Smart block selection during drag operations
function useSmartBlockSelection(editor: Editor) {
const selectRelevantBlocks = useCallback((draggedBlockId: string) => {
const allBlocks = getBlocksWithId(editor, { at: [] });
const draggedBlock = allBlocks.find(([el]) => el.id === draggedBlockId);
if (!draggedBlock) return;
// If multiple blocks are selected and dragged block is among them,
// keep the selection. Otherwise, select just the dragged block.
if (editor.selection) {
const selectedBlocks = getBlocksWithId(editor, { at: editor.selection });
const isDraggedBlockSelected = selectedBlocks.some(([el]) => el.id === draggedBlockId);
if (!isDraggedBlockSelected) {
// Select just the dragged block
const [, path] = draggedBlock;
editor.tf.select(editor.api.range(path)!);
}
}
}, [editor]);
return { selectRelevantBlocks };
}export interface GetHoverDirectionOptions {
clientOffset: { x: number; y: number };
hoveredClientRect: DOMRect;
orientation?: 'horizontal' | 'vertical';
threshold?: number;
}
export type DropLineDirection = '' | 'bottom' | 'left' | 'right' | 'top';
export type EditorNodesOptions<T> = {
at?: Location;
match?: (node: T) => boolean;
mode?: 'all' | 'highest' | 'lowest';
universal?: boolean;
reverse?: boolean;
voids?: boolean;
};
export type NodeEntry<T> = [T, Path];
export type Location = Path | Point | Range;
export type Path = number[];Install with Tessl CLI
npx tessl i tessl/npm-udecode--plate-dnd