Block Handle
Block handle components enable users to interact with, reorder, and insert blocks in your editor through drag-and-drop operations.
- examples/block-handle/editor.tsx
- examples/block-handle/extension.ts
- examples/block-handle/index.ts
- sample/default-content-drag-and-drop.ts
- ui/block-handle/block-handle.tsx
- ui/block-handle/index.ts
- ui/code-block-view/code-block-view.tsx
- ui/code-block-view/index.ts
- ui/drop-indicator/drop-indicator.tsx
- ui/drop-indicator/index.ts
import 'prosekit/basic/style.css'
import 'prosekit/basic/typography.css'
import { useMemo } from 'preact/hooks'
import { createEditor } from 'prosekit/core'
import { ProseKit } from 'prosekit/preact'
import { DEFAULT_DRAG_AND_DROP_CONTENT } from '../../sample/default-content-drag-and-drop'
import { BlockHandle } from '../../ui/block-handle'
import { DropIndicator } from '../../ui/drop-indicator'
import { defineExtension } from './extension'
export default function Editor() {
const editor = useMemo(() => {
const extension = defineExtension()
return createEditor({ extension, defaultContent: DEFAULT_DRAG_AND_DROP_CONTENT })
}, [])
return (
<ProseKit editor={editor}>
<div className="box-border h-full w-full min-h-36 overflow-y-hidden overflow-x-hidden rounded-md border border-solid border-gray-200 dark:border-gray-700 shadow-sm flex flex-col bg-white dark:bg-gray-950 text-black dark:text-white">
<div className="relative w-full flex-1 box-border overflow-y-auto">
<div ref={editor.mount} className="ProseMirror box-border min-h-full px-[max(4rem,calc(50%-20rem))] py-8 outline-hidden outline-0 [&_span[data-mention=user]]:text-blue-500 [&_span[data-mention=tag]]:text-violet-500"></div>
<BlockHandle />
<DropIndicator />
</div>
</div>
</ProseKit>
)
}import { defineBasicExtension } from 'prosekit/basic'
import { union } from 'prosekit/core'
import { defineCodeBlockView } from '../../ui/code-block-view'
export function defineExtension() {
return union([defineBasicExtension(), defineCodeBlockView()])
}
export type EditorExtension = ReturnType<typeof defineExtension>export { default as ExampleEditor } from './editor'// This is the default content for drag and drop examples.
export const DEFAULT_DRAG_AND_DROP_CONTENT = `
<h1>Drag and Drop Demo</h1>
<p>Try dragging any paragraph or heading by clicking on the handle that appears on the left when you hover over it.</p>
<h2>Getting Started</h2>
<p>Hover over any block to see the drag handle appear. Click and drag to reorder content.</p>
<p>This paragraph can be moved above or below other blocks.</p>
<h2>Different Block Types</h2>
<p>You can drag paragraphs, headings, lists, code blocks, and more.</p>
<h3>Lists Work Too</h3>
<ul>
<li>This entire list can be dragged</li>
<li>Individual list items stay together</li>
<li>Try moving this list around</li>
</ul>
<ol>
<li>Ordered lists also support dragging</li>
<li>The numbering updates automatically</li>
<li>Drag this list to see it in action</li>
</ol>
<h3>Code Blocks</h3>
<p>Even code blocks can be moved:</p>
<pre data-language="javascript"><code>// This code block can be dragged
function dragAndDrop() {
return "Easy to rearrange!"
}</code></pre>
<h2>Nested Content</h2>
<blockquote>
<p>This blockquote can be moved as a single unit.</p>
<blockquote>
<p>Nested blockquotes move together with their parent.</p>
</blockquote>
</blockquote>
<h2>Try It Yourself</h2>
<p>Practice by moving this paragraph to the top of the document.</p>
<p>Or drag this one to between the headings above.</p>
<p>The drag handles make it easy to reorganize your content exactly how you want it.</p>
`import {
BlockHandleAdd,
BlockHandleDraggable,
BlockHandlePopover,
} from 'prosekit/preact/block-handle'
export default function BlockHandle() {
return (
<BlockHandlePopover className="flex items-center flex-row box-border justify-center transition border-0 [&:not([data-state])]:hidden will-change-transform motion-safe:data-[state=open]:animate-in motion-safe:data-[state=closed]:animate-out motion-safe:data-[state=open]:fade-in-0 motion-safe:data-[state=closed]:fade-out-0 motion-safe:data-[state=open]:zoom-in-95 motion-safe:data-[state=closed]:zoom-out-95 motion-safe:data-[state=open]:animate-duration-150 motion-safe:data-[state=closed]:animate-duration-200">
<BlockHandleAdd className="flex items-center box-border justify-center h-[1.5em] w-[1.5em] hover:bg-gray-100 dark:hover:bg-gray-800 rounded-sm text-gray-500/50 dark:text-gray-500/50 cursor-pointer">
<div className="i-lucide-plus size-5 block" />
</BlockHandleAdd>
<BlockHandleDraggable className="flex items-center box-border justify-center h-[1.5em] w-[1.2em] hover:bg-gray-100 dark:hover:bg-gray-800 rounded-sm text-gray-500/50 dark:text-gray-500/50 cursor-grab">
<div className="i-lucide-grip-vertical size-5 block" />
</BlockHandleDraggable>
</BlockHandlePopover>
)
}export { default as BlockHandle } from './block-handle'import type { JSX } from 'preact'
import type { CodeBlockAttrs } from 'prosekit/extensions/code-block'
import { shikiBundledLanguagesInfo } from 'prosekit/extensions/code-block'
import type { PreactNodeViewProps } from 'prosekit/preact'
export default function CodeBlockView(props: PreactNodeViewProps) {
const attrs = props.node.attrs as CodeBlockAttrs
const language = attrs.language || ''
const setLanguage = (language: string) => {
const attrs: CodeBlockAttrs = { language }
props.setAttrs(attrs)
}
const handleChange = (
event: JSX.TargetedEvent<HTMLSelectElement, Event>,
) => {
setLanguage(event.currentTarget.value)
}
return (
<>
<div className="relative mx-2 top-3 h-0 select-none overflow-visible text-xs" contentEditable={false}>
<select
aria-label="Code block language"
className="outline-unset focus:outline-unset relative box-border w-auto cursor-pointer select-none appearance-none rounded-sm border-none bg-transparent px-2 py-1 text-xs transition text-(--prosemirror-highlight) opacity-0 hover:opacity-80 [div[data-node-view-root]:hover_&]:opacity-50 hover:[div[data-node-view-root]:hover_&]:opacity-80"
onChange={handleChange}
value={language || ''}
>
<option value="">Plain Text</option>
{shikiBundledLanguagesInfo.map((info) => (
<option key={info.id} value={info.id}>
{info.name}
</option>
))}
</select>
</div>
<pre ref={props.contentRef} data-language={language}></pre>
</>
)
}import type { Extension } from 'prosekit/core'
import {
definePreactNodeView,
type PreactNodeViewComponent,
} from 'prosekit/preact'
import CodeBlockView from './code-block-view'
export function defineCodeBlockView(): Extension {
return definePreactNodeView({
name: 'codeBlock',
contentAs: 'code',
component: CodeBlockView satisfies PreactNodeViewComponent,
})
}import { DropIndicator as BaseDropIndicator } from 'prosekit/preact/drop-indicator'
export default function DropIndicator() {
return <BaseDropIndicator className="z-50 transition-all bg-blue-500" />
}export { default as DropIndicator } from './drop-indicator'- examples/block-handle/editor.tsx
- examples/block-handle/extension.ts
- examples/block-handle/index.ts
- sample/default-content-drag-and-drop.ts
- ui/block-handle/block-handle.tsx
- ui/block-handle/index.ts
- ui/code-block-view/code-block-view.tsx
- ui/code-block-view/index.ts
- ui/drop-indicator/drop-indicator.tsx
- ui/drop-indicator/index.ts
import 'prosekit/basic/style.css'
import 'prosekit/basic/typography.css'
import { createEditor } from 'prosekit/core'
import { ProseKit } from 'prosekit/react'
import { useMemo } from 'react'
import { DEFAULT_DRAG_AND_DROP_CONTENT } from '../../sample/default-content-drag-and-drop'
import { BlockHandle } from '../../ui/block-handle'
import { DropIndicator } from '../../ui/drop-indicator'
import { defineExtension } from './extension'
export default function Editor() {
const editor = useMemo(() => {
const extension = defineExtension()
return createEditor({ extension, defaultContent: DEFAULT_DRAG_AND_DROP_CONTENT })
}, [])
return (
<ProseKit editor={editor}>
<div className="box-border h-full w-full min-h-36 overflow-y-hidden overflow-x-hidden rounded-md border border-solid border-gray-200 dark:border-gray-700 shadow-sm flex flex-col bg-white dark:bg-gray-950 text-black dark:text-white">
<div className="relative w-full flex-1 box-border overflow-y-auto">
<div ref={editor.mount} className="ProseMirror box-border min-h-full px-[max(4rem,calc(50%-20rem))] py-8 outline-hidden outline-0 [&_span[data-mention=user]]:text-blue-500 [&_span[data-mention=tag]]:text-violet-500"></div>
<BlockHandle />
<DropIndicator />
</div>
</div>
</ProseKit>
)
}import { defineBasicExtension } from 'prosekit/basic'
import { union } from 'prosekit/core'
import { defineCodeBlockView } from '../../ui/code-block-view'
export function defineExtension() {
return union([defineBasicExtension(), defineCodeBlockView()])
}
export type EditorExtension = ReturnType<typeof defineExtension>export { default as ExampleEditor } from './editor'// This is the default content for drag and drop examples.
export const DEFAULT_DRAG_AND_DROP_CONTENT = `
<h1>Drag and Drop Demo</h1>
<p>Try dragging any paragraph or heading by clicking on the handle that appears on the left when you hover over it.</p>
<h2>Getting Started</h2>
<p>Hover over any block to see the drag handle appear. Click and drag to reorder content.</p>
<p>This paragraph can be moved above or below other blocks.</p>
<h2>Different Block Types</h2>
<p>You can drag paragraphs, headings, lists, code blocks, and more.</p>
<h3>Lists Work Too</h3>
<ul>
<li>This entire list can be dragged</li>
<li>Individual list items stay together</li>
<li>Try moving this list around</li>
</ul>
<ol>
<li>Ordered lists also support dragging</li>
<li>The numbering updates automatically</li>
<li>Drag this list to see it in action</li>
</ol>
<h3>Code Blocks</h3>
<p>Even code blocks can be moved:</p>
<pre data-language="javascript"><code>// This code block can be dragged
function dragAndDrop() {
return "Easy to rearrange!"
}</code></pre>
<h2>Nested Content</h2>
<blockquote>
<p>This blockquote can be moved as a single unit.</p>
<blockquote>
<p>Nested blockquotes move together with their parent.</p>
</blockquote>
</blockquote>
<h2>Try It Yourself</h2>
<p>Practice by moving this paragraph to the top of the document.</p>
<p>Or drag this one to between the headings above.</p>
<p>The drag handles make it easy to reorganize your content exactly how you want it.</p>
`import {
BlockHandleAdd,
BlockHandleDraggable,
BlockHandlePopover,
} from 'prosekit/react/block-handle'
export default function BlockHandle() {
return (
<BlockHandlePopover className="flex items-center flex-row box-border justify-center transition border-0 [&:not([data-state])]:hidden will-change-transform motion-safe:data-[state=open]:animate-in motion-safe:data-[state=closed]:animate-out motion-safe:data-[state=open]:fade-in-0 motion-safe:data-[state=closed]:fade-out-0 motion-safe:data-[state=open]:zoom-in-95 motion-safe:data-[state=closed]:zoom-out-95 motion-safe:data-[state=open]:animate-duration-150 motion-safe:data-[state=closed]:animate-duration-200">
<BlockHandleAdd className="flex items-center box-border justify-center h-[1.5em] w-[1.5em] hover:bg-gray-100 dark:hover:bg-gray-800 rounded-sm text-gray-500/50 dark:text-gray-500/50 cursor-pointer">
<div className="i-lucide-plus size-5 block" />
</BlockHandleAdd>
<BlockHandleDraggable className="flex items-center box-border justify-center h-[1.5em] w-[1.2em] hover:bg-gray-100 dark:hover:bg-gray-800 rounded-sm text-gray-500/50 dark:text-gray-500/50 cursor-grab">
<div className="i-lucide-grip-vertical size-5 block" />
</BlockHandleDraggable>
</BlockHandlePopover>
)
}export { default as BlockHandle } from './block-handle'import type { CodeBlockAttrs } from 'prosekit/extensions/code-block'
import { shikiBundledLanguagesInfo } from 'prosekit/extensions/code-block'
import type { ReactNodeViewProps } from 'prosekit/react'
export default function CodeBlockView(props: ReactNodeViewProps) {
const attrs = props.node.attrs as CodeBlockAttrs
const language = attrs.language
const setLanguage = (language: string) => {
const attrs: CodeBlockAttrs = { language }
props.setAttrs(attrs)
}
return (
<>
<div className="relative mx-2 top-3 h-0 select-none overflow-visible text-xs" contentEditable={false}>
<select
aria-label="Code block language"
className="outline-unset focus:outline-unset relative box-border w-auto cursor-pointer select-none appearance-none rounded-sm border-none bg-transparent px-2 py-1 text-xs transition text-(--prosemirror-highlight) opacity-0 hover:opacity-80 [div[data-node-view-root]:hover_&]:opacity-50 hover:[div[data-node-view-root]:hover_&]:opacity-80"
onChange={(event) => setLanguage(event.target.value)}
value={language || ''}
>
<option value="">Plain Text</option>
{shikiBundledLanguagesInfo.map((info) => (
<option key={info.id} value={info.id}>
{info.name}
</option>
))}
</select>
</div>
<pre ref={props.contentRef} data-language={language}></pre>
</>
)
}import type { Extension } from 'prosekit/core'
import {
defineReactNodeView,
type ReactNodeViewComponent,
} from 'prosekit/react'
import CodeBlockView from './code-block-view'
export function defineCodeBlockView(): Extension {
return defineReactNodeView({
name: 'codeBlock',
contentAs: 'code',
component: CodeBlockView satisfies ReactNodeViewComponent,
})
}import { DropIndicator as BaseDropIndicator } from 'prosekit/react/drop-indicator'
export default function DropIndicator() {
return <BaseDropIndicator className="z-50 transition-all bg-blue-500" />
}export { default as DropIndicator } from './drop-indicator'Overview
Section titled “Overview”The block handle system provides two main capabilities:
- Block manipulation: Add new blocks or drag existing blocks to reorder content
- Visual feedback: Show users exactly where content will be placed during drag operations
Components
Section titled “Components”Block Handle
Section titled “Block Handle”The BlockHandle consists of three main components:
BlockHandlePopover: A popover container that appears on the left side when hovering over a blockBlockHandleAdd: An add button (+) that inserts a new block below the current oneBlockHandleDraggable: A drag handle (⋮⋮) that allows users to reorder blocks by dragging
You can use BlockHandleAdd, BlockHandleDraggable, or both together depending on your needs.
Drop Indicator
Section titled “Drop Indicator”The DropIndicator provides visual feedback during drag-and-drop operations by showing a horizontal line where the dragged content will be inserted. It automatically appears when dragging and disappears when the drag operation completes.
Installation
Section titled “Installation”Copy and paste the component source files linked above into your project.
API Reference
Section titled “API Reference”- block-handle
- drop-indicator