'use client' /** * Poignée drag globale (Novel / tiptap-extension-global-drag-handle). * L'extension TipTap positionne cet élément ; le clic ouvre le menu bloc. */ import { memo, useEffect, useRef } from 'react' import type { Editor } from '@tiptap/core' import { BLOCK_DRAG_HANDLE_ID, resolveBlockAtDragHandle } from '@/lib/editor/block-at-drag-handle' type EditorBlockDragHandleProps = { editor: Editor | null onOpenMenu: (anchorRect: DOMRect) => void } export const EditorBlockDragHandle = memo(function EditorBlockDragHandle({ editor, onOpenMenu, }: EditorBlockDragHandleProps) { const dragStartedRef = useRef(false) const onOpenMenuRef = useRef(onOpenMenu) onOpenMenuRef.current = onOpenMenu useEffect(() => { if (!editor || editor.isDestroyed) return const el = document.getElementById(BLOCK_DRAG_HANDLE_ID) if (!el) return const onPointerDown = () => { dragStartedRef.current = false } const onDragStart = () => { dragStartedRef.current = true } const onClick = (e: MouseEvent) => { if (dragStartedRef.current) { dragStartedRef.current = false return } e.preventDefault() e.stopPropagation() const block = resolveBlockAtDragHandle(editor) if (!block) return onOpenMenuRef.current(el.getBoundingClientRect()) } const clampPosition = () => { if (el.classList.contains('hide') || el.style.display === 'none') return const rect = el.getBoundingClientRect() if (rect.height === 0) return const overshootBottom = rect.bottom - window.innerHeight + 10 const overshootTop = 10 - rect.top if (overshootBottom > 0) { const currentTop = parseFloat(el.style.top) || 0 el.style.top = `${currentTop - overshootBottom}px` } else if (overshootTop > 0) { const currentTop = parseFloat(el.style.top) || 0 el.style.top = `${currentTop + overshootTop}px` } } el.addEventListener('pointerdown', onPointerDown) el.addEventListener('dragstart', onDragStart) el.addEventListener('click', onClick) const observer = new MutationObserver(clampPosition) observer.observe(el, { attributes: true, attributeFilter: ['style'] }) window.addEventListener('scroll', clampPosition, { passive: true }) window.addEventListener('resize', clampPosition) return () => { el.removeEventListener('pointerdown', onPointerDown) el.removeEventListener('dragstart', onDragStart) el.removeEventListener('click', onClick) observer.disconnect() window.removeEventListener('scroll', clampPosition) window.removeEventListener('resize', clampPosition) } }, [editor]) return (
) })