diff --git a/src/shared/components/BlockEditor/Block.client.js b/src/shared/components/BlockEditor/Block.client.js index 1e4e6d4..16eab03 100644 --- a/src/shared/components/BlockEditor/Block.client.js +++ b/src/shared/components/BlockEditor/Block.client.js @@ -205,6 +205,17 @@ const Block = forwardRef(function Block( } } + function handleFocus() { + if (def?.isText && !disabled) { + const el = editableRef.current; + const text = el?.textContent ?? ''; + if (text.startsWith('/') && !text.slice(1).includes(' ')) { + onSlashOpen?.({ blockId: block.id, query: text.slice(1) }); + } + } + onFocus?.(block.id); + } + function handlePaste(e) { // MVP : on colle uniquement du texte brut pour éviter le HTML externe. e.preventDefault(); @@ -324,7 +335,7 @@ const Block = forwardRef(function Block( onInput={handleInput} onKeyDown={handleKeyDown} onPaste={handlePaste} - onFocus={() => onFocus?.(block.id)} + onFocus={handleFocus} /> ) : ( @@ -336,7 +347,7 @@ const Block = forwardRef(function Block( onInput={handleInput} onKeyDown={handleKeyDown} onPaste={handlePaste} - onFocus={() => onFocus?.(block.id)} + onFocus={handleFocus} /> ) ) : def.Component ? ( diff --git a/src/shared/components/BlockEditor/BlockEditor.client.js b/src/shared/components/BlockEditor/BlockEditor.client.js index 4c46469..cba4b0a 100644 --- a/src/shared/components/BlockEditor/BlockEditor.client.js +++ b/src/shared/components/BlockEditor/BlockEditor.client.js @@ -71,6 +71,7 @@ export default function BlockEditor({ const blocks = useMemo(() => ensureNonEmpty(value), [value]); const blockRefs = useRef(new Map()); const containerRef = useRef(null); + const outerRef = useRef(null); const [focusBlockId, setFocusBlockId] = useState(null); const [focusOffset, setFocusOffset] = useState(null); const [selectedBlockIds, setSelectedBlockIds] = useState(() => new Set()); @@ -764,6 +765,26 @@ export default function BlockEditor({ selectAllBlocks(); } + // Ferme le slash menu quand on clique en dehors de l'éditeur ou sur un + // bloc différent de celui qui a ouvert le menu. + useEffect(() => { + if (!slashState) return; + function onDocMouseDown(e) { + if (e.target instanceof Element && e.target.closest('[data-slash-menu]')) return; + const outer = outerRef.current; + if (!outer || !outer.contains(e.target)) { + setSlashState(null); + return; + } + const blockEl = e.target instanceof Element ? e.target.closest('[data-block-id]') : null; + if (!blockEl || blockEl.getAttribute('data-block-id') !== slashState.blockId) { + setSlashState(null); + } + } + document.addEventListener('mousedown', onDocMouseDown, true); + return () => document.removeEventListener('mousedown', onDocMouseDown, true); + }, [slashState]); + // Le slash menu utilise un listener au niveau document en phase capture pour // intercepter les touches avant que le contentEditable ne gère ses défauts // (sinon ↑/↓ déplacent le caret et Entrée insère une nouvelle ligne). @@ -804,7 +825,7 @@ export default function BlockEditor({ }, [slashState, enabledBlocks]); return ( -