import { useEffect } from 'react' import type React from 'react' import { toast } from 'sonner' import { useDominoStore, useDominoStoreInstance, type ImageElement, type TextElement, } from '../components/canvas' import { copyImageToClipboard, copyTextElementToClipboard, } from '../utils/helper' import { useZoomActions } from './use-zoom-actions' export interface UseShortcutsProps { rootRef: React.RefObject onDelete?: (ids: string[]) => void onPaste?: (e: ClipboardEvent) => void } export function useShortcuts({ rootRef, onDelete, onPaste }: UseShortcutsProps) { const store = useDominoStoreInstance() const { stepZoom } = useZoomActions(rootRef) const mode = useDominoStore(s => s.mode) const setMode = useDominoStore(s => s.setMode) const setViewport = useDominoStore(s => s.setViewport) const undo = useDominoStore(s => s.undo) const redo = useDominoStore(s => s.redo) const selectedIds = useDominoStore(s => s.selectedIds) const focusedElementId = useDominoStore(s => s.focusedElementId) const readOnly = useDominoStore(s => s.readOnly) useEffect(() => { const isInputActive = () => { // 1. 如果有元素正在被聚焦(比如正在编辑文字),禁用快捷键 if (focusedElementId) { const { elements } = store.getState() const focusedEl = elements[focusedElementId] if (focusedEl?.type === 'text') return true } // 2. 如果原生 DOM 的输入框处于激活状态,禁用快捷键 const activeElement = document.activeElement if (!activeElement) return false return ( activeElement.tagName.toLowerCase() === 'input' || activeElement.tagName.toLowerCase() === 'textarea' || (activeElement as HTMLElement).isContentEditable ) } const handleKeyDown = (e: KeyboardEvent) => { if (isInputActive()) return const isMod = e.metaKey || e.ctrlKey const isShift = e.shiftKey // 1. Space -> Pan Mode if (e.code === 'Space' && mode !== 'pan') { e.preventDefault() setMode('pan') } // 2. Undo/Redo if (isMod && e.key.toLowerCase() === 'z') { e.preventDefault() if (isShift) redo() else undo() } else if (isMod && e.key.toLowerCase() === 'y') { e.preventDefault() redo() } // 3. Zoom Shortcuts if (isMod && (e.key === '=' || e.key === '+')) { e.preventDefault() stepZoom(1) } if (isMod && (e.key === '-' || e.key === '_')) { e.preventDefault() stepZoom(-1) } if (e.key === '0') { e.preventDefault() setViewport({ scale: 1 }) } // 4. Delete if (e.key === 'Delete' || e.key === 'Backspace') { if (selectedIds.length > 0 && !readOnly) { e.preventDefault() onDelete?.(selectedIds) } } // 5. Select All if (isMod && e.key.toLowerCase() === 'a') { e.preventDefault() const { elements, setSelectedIds } = store.getState() setSelectedIds(Object.keys(elements)) } // 6. Escape if (e.key === 'Escape') { const { setSelectedIds, setFocusedElementId } = store.getState() setSelectedIds([]) setFocusedElementId(null) if (mode !== 'select') setMode('select') } // 7. Arrow Keys const ARROW_KEYS = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'] if (ARROW_KEYS.includes(e.key) && selectedIds.length > 0 && !readOnly) { e.preventDefault() const step = isShift ? 10 : 1 const dx = e.key === 'ArrowLeft' ? -step : e.key === 'ArrowRight' ? step : 0 const dy = e.key === 'ArrowUp' ? -step : e.key === 'ArrowDown' ? step : 0 const { moveElements } = store.getState() moveElements(selectedIds, dx, dy) } // 8. Copy if (isMod && e.key.toLowerCase() === 'c') { const { elements } = store.getState() const selectedElements = selectedIds .map(id => elements[id]) .filter(Boolean) if (selectedElements.length === 0) return e.preventDefault() if (selectedElements.length === 1) { const el = selectedElements[0] if (el.type === 'text') { const textEl = el as TextElement copyTextElementToClipboard(textEl).then(success => { if (success) toast.success('已复制文本') }) } else if (el.type === 'image') { const imageEl = el as ImageElement copyImageToClipboard(imageEl.id).then(success => { if (success) toast.success('已复制图片') else toast.error('复制失败') }) } return } const texts = selectedElements .filter((el): el is TextElement => el.type === 'text') .map(el => el.content) if (texts.length > 0) { navigator.clipboard .writeText(texts.join('\n')) .then(() => toast.success(`已复制 ${texts.length} 条内容`)) } } } const handleKeyUp = (e: KeyboardEvent) => { if (e.code === 'Space' && !isInputActive()) { setMode('select') } } const handlePaste = (e: ClipboardEvent) => { if (isInputActive()) return if (!onPaste) return onPaste(e) } window.addEventListener('keydown', handleKeyDown) window.addEventListener('keyup', handleKeyUp) window.addEventListener('paste', handlePaste) return () => { window.removeEventListener('keydown', handleKeyDown) window.removeEventListener('keyup', handleKeyUp) window.removeEventListener('paste', handlePaste) } }, [ stepZoom, mode, setMode, setViewport, undo, redo, selectedIds, focusedElementId, readOnly, store, rootRef, onDelete, onPaste, ]) }