Files
Momento/memento-note/components/editor-block-drag-handle.tsx
Antigravity f7b62009cf
Some checks failed
CI / Lint, Unit Tests & Build (push) Failing after 1m24s
CI / Deploy production (on server) (push) Has been skipped
fix: drag handle resolve container blocks + menu popup clamp viewport
- 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
2026-06-14 19:01:30 +00:00

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>
)
})