- Drag handle résout les blocs conteneurs (columns, toggle, callout) au lieu du paragraphe intérieur - Delete agit sur tout le bloc conteneur, pas juste le paragraphe - Menu popup (block action menu) clampé dans le viewport (Math.min + overflow auto) - Drag handle clamped dans le viewport via MutationObserver + scroll/resize
102 lines
3.2 KiB
TypeScript
102 lines
3.2 KiB
TypeScript
'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 (
|
|
<div
|
|
id={BLOCK_DRAG_HANDLE_ID}
|
|
className="drag-handle hide"
|
|
role="button"
|
|
tabIndex={-1}
|
|
aria-hidden="true"
|
|
>
|
|
<svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor" aria-hidden="true">
|
|
<circle cx="4" cy="3" r="1.2" />
|
|
<circle cx="10" cy="3" r="1.2" />
|
|
<circle cx="4" cy="7" r="1.2" />
|
|
<circle cx="10" cy="7" r="1.2" />
|
|
<circle cx="4" cy="11" r="1.2" />
|
|
<circle cx="10" cy="11" r="1.2" />
|
|
</svg>
|
|
</div>
|
|
)
|
|
})
|