The SelectionBox component renders a visual selection rectangle when dragging to select multiple blocks. It provides visual feedback during multi-block selection operations.
Installation
Basic Usage
import YooptaEditor, { createYooptaEditor } from '@yoopta/editor';
import { SelectionBox } from '@yoopta/ui';
import { useMemo, useRef } from 'react';
function MyEditor() {
const editor = useMemo(() => createYooptaEditor({ plugins }), []);
const containerRef = useRef<HTMLDivElement>(null);
return (
<div ref={containerRef}>
<YooptaEditor editor={editor}>
<SelectionBox selectionBoxElement={containerRef} />
</YooptaEditor>
</div>
);
}
Component API
SelectionBox
selectionBoxElement
HTMLElement | { current: HTMLElement | null } | null
Container element to constrain selection box (typically the editor container)
How It Works
The selection box:
- Activates when user clicks and drags in the editor
- Renders a semi-transparent blue rectangle showing the selection area
- Updates position and size as the mouse moves
- Selects blocks that intersect with the rectangle
- Deactivates when the mouse button is released
Examples
Basic Setup
import { SelectionBox } from '@yoopta/ui';
import YooptaEditor from '@yoopta/editor';
import { useRef } from 'react';
function Editor() {
const containerRef = useRef<HTMLDivElement>(null);
return (
<div ref={containerRef} className="editor-container">
<YooptaEditor editor={editor}>
<SelectionBox selectionBoxElement={containerRef} />
</YooptaEditor>
</div>
);
}
Without Container Ref
If no container is provided, selection works on the entire document:
import { SelectionBox } from '@yoopta/ui';
function Editor() {
return (
<YooptaEditor editor={editor}>
<SelectionBox />
</YooptaEditor>
);
}
With Custom Container
import { SelectionBox } from '@yoopta/ui';
import { useRef } from 'react';
function Editor() {
const scrollContainerRef = useRef<HTMLDivElement>(null);
return (
<div
ref={scrollContainerRef}
style={{ height: '600px', overflow: 'auto' }}
>
<YooptaEditor editor={editor}>
<SelectionBox selectionBoxElement={scrollContainerRef} />
</YooptaEditor>
</div>
);
}
With Drag and Drop
Combine with block drag and drop for a complete experience:
import { SelectionBox } from '@yoopta/ui';
import { BlockDndContext, SortableBlock } from '@yoopta/ui';
import { useRef, useCallback } from 'react';
import type { RenderBlockProps } from '@yoopta/editor';
function Editor() {
const containerRef = useRef<HTMLDivElement>(null);
const renderBlock = useCallback(
({ children, blockId }: RenderBlockProps) => (
<SortableBlock id={blockId} useDragHandle>
{children}
</SortableBlock>
),
[]
);
return (
<div ref={containerRef}>
<BlockDndContext editor={editor}>
<YooptaEditor editor={editor} renderBlock={renderBlock}>
<SelectionBox selectionBoxElement={containerRef} />
</YooptaEditor>
</BlockDndContext>
</div>
);
}
useRectangeSelectionBox Hook
For advanced use cases, you can use the underlying hook:
import { useRectangeSelectionBox } from '@yoopta/ui';
import { useYooptaEditor } from '@yoopta/editor';
import { useRef } from 'react';
import type { RectangeSelectionProps } from '@yoopta/ui';
function CustomSelectionBox() {
const editor = useYooptaEditor();
const containerRef = useRef<HTMLDivElement>(null);
const { coords, origin, selection } = useRectangeSelectionBox({
editor,
root: containerRef.current ?? undefined,
});
if (!selection) return null;
return (
<div
style={{
position: 'fixed',
left: origin[0],
top: origin[1],
width: Math.abs(coords[0] - origin[0]),
height: Math.abs(coords[1] - origin[1]),
backgroundColor: 'rgba(0, 100, 255, 0.1)',
border: '2px solid rgb(0, 100, 255)',
pointerEvents: 'none',
}}
/>
);
}
Hook Return Type
type RectangeSelectionState = {
origin: [number, number]; // Starting point (page coordinates)
coords: [number, number]; // Current point (viewport coordinates)
selection: boolean; // Whether selection is active
};
Behavior
Selection Activation
Selection activates when:
- User clicks in empty space (not on text or interactive elements)
- Mouse is dragged while holding the button
- Not in read-only mode
Selection Logic
Blocks are selected when:
- The selection rectangle intersects with the block’s bounding box
- Partial overlap is sufficient for selection
- Selection state updates in real-time during drag
Deactivation
Selection deactivates when:
- Mouse button is released
- User presses Escape
- Click occurs outside the editor
Coordinates
- Origin: Stored in page coordinates (accounts for initial scroll position)
- Coords: Stored in viewport coordinates (updates during scroll)
- Position: Rendered using
position: fixed for smooth scrolling
Styling
Default Styles
The selection box has these default styles:
{
position: fixed;
background-color: rgba(35, 131, 226, 0.14);
border: 1px solid rgba(35, 131, 226, 0.4);
pointer-events: none;
z-index: 10;
}
Custom Styling
Create a custom selection box component:
import { useRectangeSelectionBox } from '@yoopta/ui';
import { useYooptaEditor, useYooptaReadOnly } from '@yoopta/editor';
import { useMemo } from 'react';
function CustomSelectionBox({ containerRef }) {
const editor = useYooptaEditor();
const isReadOnly = useYooptaReadOnly();
const { coords, origin, selection } = useRectangeSelectionBox({
editor,
root: containerRef?.current,
});
const originViewport: [number, number] = useMemo(
() => [
origin[0] - window.scrollX,
origin[1] - window.scrollY,
],
[origin, coords]
);
if (!selection || isReadOnly) return null;
return (
<div
style={{
position: 'fixed',
left: originViewport[0],
top: originViewport[1],
width: Math.abs(coords[0] - originViewport[0]),
height: Math.abs(coords[1] - originViewport[1]),
backgroundColor: 'rgba(255, 100, 0, 0.1)',
border: '2px dashed rgb(255, 100, 0)',
borderRadius: '4px',
pointerEvents: 'none',
zIndex: 100,
}}
/>
);
}
Read-Only Mode
The selection box is automatically hidden in read-only mode:
import { SelectionBox } from '@yoopta/ui';
function Editor() {
return (
<YooptaEditor editor={editor} readOnly={true}>
{/* SelectionBox won't render when readOnly={true} */}
<SelectionBox />
</YooptaEditor>
);
}
Multi-Block Operations
Once blocks are selected, you can perform bulk operations:
import { useYooptaEditor } from '@yoopta/editor';
import { useEffect } from 'react';
function useMultiBlockActions() {
const editor = useYooptaEditor();
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
const selectedPaths = editor.path.selected;
if (!Array.isArray(selectedPaths) || selectedPaths.length === 0) {
return;
}
// Delete selected blocks
if (e.key === 'Backspace' || e.key === 'Delete') {
e.preventDefault();
editor.batchOperations(() => {
selectedPaths.forEach((path) => {
const blockId = Object.keys(editor.children).find(
(id) => editor.children[id].meta.order === path
);
if (blockId) {
editor.deleteBlock(blockId);
}
});
});
}
// Copy selected blocks
if ((e.metaKey || e.ctrlKey) && e.key === 'c') {
// Copy logic
}
};
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [editor]);
}
TypeScript
import type {
SelectionBoxProps,
RectangeSelectionProps,
RectangeSelectionState,
SelectionBoxRoot,
} from '@yoopta/ui';
See Also