import { useEffect, useRef } from 'react'; import { useEditorStore } from '@/stores/editorStore'; import { useUiStore } from '@/stores/uiStore'; /** Vrai si l'utilisateur est en train de saisir du texte. */ function isTypingTarget(target: EventTarget | null): boolean { if (!(target instanceof HTMLElement)) return false; const tag = target.tagName; return ( tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || target.isContentEditable === true ); } export interface ShortcutHandlers { onSave: () => void; onFit: () => void; onFocusSearch: () => void; } /** * Raccourcis clavier de l'editeur. * Ctrl et Cmd (macOS) sont traites de la meme maniere. */ export function useEditorShortcuts(enabled: boolean, handlers: ShortcutHandlers): void { // Les handlers changent a chaque rendu : on les lit via une ref pour ne // reinscrire l'ecouteur qu'une seule fois. const handlersRef = useRef(handlers); handlersRef.current = handlers; useEffect(() => { if (!enabled) return undefined; const onKeyDown = (event: KeyboardEvent) => { const store = useEditorStore.getState(); const typing = isTypingTarget(event.target) || store.editingNodeId !== null; const modifier = event.ctrlKey || event.metaKey; const key = event.key.toLowerCase(); if (modifier && key === 's') { event.preventDefault(); handlersRef.current.onSave(); return; } if (modifier && key === 'f') { event.preventDefault(); handlersRef.current.onFocusSearch(); return; } if (typing) return; if (modifier && event.shiftKey && (key === 'c' || key === 'v')) { event.preventDefault(); if (key === 'c') { if (store.copyStyle()) useUiStore.getState().pushToast('Style copie.', 'info'); return; } const applied = store.pasteStyle(); if (applied > 0) { useUiStore.getState().pushToast(`Style applique a ${applied} noeud(s).`, 'success'); } return; } if (modifier) { switch (key) { case 'z': event.preventDefault(); if (event.shiftKey) store.redo(); else store.undo(); return; case 'y': event.preventDefault(); store.redo(); return; case 'c': if (store.copySelection() > 0) { useUiStore.getState().pushToast('Selection copiee.', 'info'); } return; case 'v': event.preventDefault(); store.paste(); return; case 'd': event.preventDefault(); store.duplicateSelection(); return; case 'a': event.preventDefault(); store.setSelection(store.map?.nodes.map((node) => node.id) ?? []); return; default: return; } } switch (event.key) { case 'Tab': event.preventDefault(); store.addChild(); break; case 'Enter': event.preventDefault(); store.addSibling(); break; case 'Delete': case 'Backspace': if (store.selectedNodeIds.length > 0) { event.preventDefault(); store.deleteNodes(store.selectedNodeIds); } break; case 'f': case 'F': event.preventDefault(); handlersRef.current.onFit(); break; case 'F2': if (store.selectedNodeIds[0]) { event.preventDefault(); store.setEditingNode(store.selectedNodeIds[0]); } break; case 'Escape': store.setEditingNode(null); store.setSelection([]); break; default: break; } }; window.addEventListener('keydown', onKeyDown); return () => window.removeEventListener('keydown', onKeyDown); }, [enabled]); }