初始化模版工程
This commit is contained in:
200
components/image-editor/hooks/use-shortcuts.ts
Normal file
200
components/image-editor/hooks/use-shortcuts.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
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<HTMLDivElement | null>
|
||||
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,
|
||||
])
|
||||
}
|
||||
Reference in New Issue
Block a user