UNPKG

tldraw

Version:

A tiny little drawing editor.

211 lines (178 loc) • 5.47 kB
import { Editor, TLPointerEventInfo, isAccelKey, preventDefault, useEditor, useValue, } from '@tldraw/editor' import hotkeys from 'hotkeys-js' import { useEffect } from 'react' import { useActions } from '../context/actions' import { useReadonly } from './useReadonly' import { useTools } from './useTools' const SKIP_KBDS = [ // we set these in useNativeClipboardEvents instead 'copy', 'cut', 'paste', // There's also an upload asset action, so we don't want to set the kbd twice 'asset', ] /** @public */ export function useKeyboardShortcuts() { const editor = useEditor() const isReadonlyMode = useReadonly() const actions = useActions() const tools = useTools() const isFocused = useValue('is focused', () => editor.getInstanceState().isFocused, [editor]) useEffect(() => { if (!isFocused) return const disposables = new Array<() => void>() const container = editor.getContainer() const hot = (keys: string, callback: (event: KeyboardEvent) => void) => { hotkeys(keys, { element: container.ownerDocument.body }, callback) disposables.push(() => { hotkeys.unbind(keys, callback) }) } const hotUp = (keys: string, callback: (event: KeyboardEvent) => void) => { hotkeys( keys, { element: container.ownerDocument.body, keyup: true, keydown: false }, callback ) disposables.push(() => { hotkeys.unbind(keys, callback) }) } // Add hotkeys for actions and tools. // Except those that in SKIP_KBDS! for (const action of Object.values(actions)) { if (!action.kbd) continue if (isReadonlyMode && !action.readonlyOk) continue if (SKIP_KBDS.includes(action.id)) continue hot(getHotkeysStringFromKbd(action.kbd), (event) => { if (areShortcutsDisabled(editor) && !action.isRequiredA11yAction) return preventDefault(event) action.onSelect('kbd') }) } for (const tool of Object.values(tools)) { if (!tool.kbd || (!tool.readonlyOk && editor.getIsReadonly())) { continue } if (SKIP_KBDS.includes(tool.id)) continue hot(getHotkeysStringFromKbd(tool.kbd), (event) => { if (areShortcutsDisabled(editor)) return preventDefault(event) tool.onSelect('kbd') }) } hot(',', (e) => { // Skip if shortcuts are disabled if (areShortcutsDisabled(editor)) return // Don't press again if already pressed if (editor.inputs.keys.has('Comma')) return preventDefault(e) // prevent whatever would normally happen editor.focus() // Focus if not already focused editor.inputs.keys.add('Comma') const { x, y, z } = editor.inputs.getCurrentPagePoint() const screenpoints = editor.pageToScreen({ x, y }) const info: TLPointerEventInfo = { type: 'pointer', name: 'pointer_down', point: { x: screenpoints.x, y: screenpoints.y, z }, shiftKey: e.shiftKey, altKey: e.altKey, ctrlKey: e.metaKey || e.ctrlKey, metaKey: e.metaKey, accelKey: isAccelKey(e), pointerId: 0, button: 0, isPen: editor.getInstanceState().isPenMode, target: 'canvas', } editor.dispatch(info) }) hotUp(',', (e) => { if (areShortcutsDisabled(editor)) return if (!editor.inputs.keys.has('Comma')) return editor.inputs.keys.delete('Comma') const { x, y, z } = editor.inputs.getCurrentScreenPoint() const info: TLPointerEventInfo = { type: 'pointer', name: 'pointer_up', point: { x, y, z }, shiftKey: e.shiftKey, altKey: e.altKey, ctrlKey: e.metaKey || e.ctrlKey, metaKey: e.metaKey, accelKey: isAccelKey(e), pointerId: 0, button: 0, isPen: editor.getInstanceState().isPenMode, target: 'canvas', } editor.dispatch(info) }) return () => { disposables.forEach((d) => d()) } }, [actions, tools, isReadonlyMode, editor, isFocused]) } export function areShortcutsDisabled(editor: Editor) { return ( editor.menus.hasAnyOpenMenus() || editor.getEditingShapeId() !== null || editor.getCrashingError() || !editor.user.getAreKeyboardShortcutsEnabled() ) } // The "raw" kbd here will look something like "a" or a combination of keys "del,backspace". // We need to first split them up by comma, then parse each key to ensure backwards compatibility // with the old kbd format. We used to have symbols to denote cmd/alt/shift, // using ! for shift, $ for cmd, and ? for alt. function getHotkeysStringFromKbd(kbd: string) { return getKeys(kbd) .map((kbd) => { let str = '' const shift = kbd.includes('!') const alt = kbd.includes('?') const cmd = kbd.includes('$') // remove the modifiers; the remaining string are the actual key const k = kbd.replace(/[!?$]/g, '') if (shift && alt && cmd) { str = `cmd+shift+alt+${k},ctrl+shift+alt+${k}` } else if (shift && cmd) { str = `cmd+shift+${k},ctrl+shift+${k}` } else if (alt && cmd) { str = `cmd+alt+${k},ctrl+alt+${k}` } else if (alt && shift) { str = `shift+alt+${k}` } else if (shift) { str = `shift+${k}` } else if (alt) { str = `alt+${k}` } else if (cmd) { str = `cmd+${k},ctrl+${k}` } else { str = k } return str }) .join(',') } // Logic to split kbd string from hotkeys-js util. function getKeys(key: string) { if (typeof key !== 'string') key = '' key = key.replace(/\s/g, '') const keys = key.split(',') let index = keys.lastIndexOf('') for (; index >= 0; ) { keys[index - 1] += ',' keys.splice(index, 1) index = keys.lastIndexOf('') } return keys }