import type { ComputePositionConfig, VirtualElement } from '@floating-ui/dom' import { type Editor, Extension } from '@tiptap/core' import type { Node } from '@tiptap/pm/model' import { DragHandlePlugin } from './drag-handle-plugin.js' import { normalizeNestedOptions } from './helpers/normalizeOptions.js' import type { NestedOptions } from './types/options.js' export const defaultComputePositionConfig: ComputePositionConfig = { placement: 'left-start', strategy: 'absolute', } export interface DragHandleOptions { /** * Renders an element that is positioned with the floating-ui/dom package */ render(): HTMLElement /** * Configuration for position computation of the drag handle * using the floating-ui/dom package */ computePositionConfig?: ComputePositionConfig /** * A function that returns the virtual element for the drag handle. * This is useful when the menu needs to be positioned relative to a specific DOM element. */ getReferencedVirtualElement?: () => VirtualElement | null /** * Locks the draghandle in place and visibility */ locked?: boolean /** * Returns a node or null when a node is hovered over */ onNodeChange?: (options: { node: Node | null; editor: Editor }) => void /** * The callback function that will be called when drag start. */ onElementDragStart?: (e: DragEvent) => void /** * The callback function that will be called when drag end. */ onElementDragEnd?: (e: DragEvent) => void /** * Enable drag handles for nested content (list items, blockquotes, etc.). * * When enabled, the drag handle appears for block nodes at any depth, not just * top-level blocks. A rule-based scoring system evaluates all ancestor nodes * at the cursor position and selects the best drag target. * * **Values:** * - `false` (default): Only root-level blocks show drag handles * - `true`: Enable with sensible defaults (left edge detection, default rules) * - `NestedOptions`: Enable with full custom configuration * * @default false * * @example * // Simple enable with sensible defaults * DragHandle.configure({ * nested: true, * }) * * @example * // Restrict to specific containers * DragHandle.configure({ * nested: { * allowedContainers: ['bulletList', 'orderedList'], * }, * }) * * @example * // With custom rules and edge detection disabled * DragHandle.configure({ * nested: { * rules: [{ * id: 'excludeCodeBlocks', * evaluate: ({ node }) => node.type.name === 'codeBlock' ? 1000 : 0, * }], * edgeDetection: 'none', * }, * }) * * @example * // Full configuration * DragHandle.configure({ * nested: { * defaultRules: true, * allowedContainers: ['bulletList', 'orderedList', 'blockquote'], * edgeDetection: { threshold: 20 }, * rules: [{ * id: 'preferShallow', * evaluate: ({ depth }) => depth * 200, * }], * }, * }) */ nested?: boolean | NestedOptions /** * Limit the drag image clone to a subset of CSS properties instead of * copying all computed styles. When set, only the listed properties * are read from `getComputedStyle` and applied to the drag image clone. * * Useful for improving drag performance on selections containing complex * or deeply nested nodes. * * @example * // Only copy visual appearance, skip layout properties * DragHandle.configure({ * dragImageProperties: ['color', 'background-color', 'font-size', 'font-family'], * }) */ dragImageProperties?: string[] } declare module '@tiptap/core' { interface Commands { dragHandle: { /** * Locks the draghandle in place and visibility */ lockDragHandle: () => ReturnType /** * Unlocks the draghandle */ unlockDragHandle: () => ReturnType /** * Toggle draghandle lock state */ toggleDragHandle: () => ReturnType } } } export const DragHandle = Extension.create({ name: 'dragHandle', addOptions() { return { render() { const element = document.createElement('div') element.classList.add('drag-handle') return element }, computePositionConfig: {}, locked: false, onNodeChange: () => { return null }, onElementDragStart: undefined, onElementDragEnd: undefined, nested: false, dragImageProperties: undefined, } }, addCommands() { return { lockDragHandle: () => ({ editor }) => { this.options.locked = true return editor.commands.setMeta('lockDragHandle', this.options.locked) }, unlockDragHandle: () => ({ editor }) => { this.options.locked = false return editor.commands.setMeta('lockDragHandle', this.options.locked) }, toggleDragHandle: () => ({ editor }) => { this.options.locked = !this.options.locked return editor.commands.setMeta('lockDragHandle', this.options.locked) }, } }, addProseMirrorPlugins() { const element = this.options.render() const nestedOptions = normalizeNestedOptions(this.options.nested) return [ DragHandlePlugin({ computePositionConfig: { ...defaultComputePositionConfig, ...this.options.computePositionConfig, }, getReferencedVirtualElement: this.options.getReferencedVirtualElement, element, editor: this.editor, onNodeChange: this.options.onNodeChange, onElementDragStart: this.options.onElementDragStart, onElementDragEnd: this.options.onElementDragEnd, nestedOptions, dragImageProperties: this.options.dragImageProperties, }).plugin, ] }, })