All files / src/composables useHotkeys.js

64% Statements 16/25
39.13% Branches 9/23
66.66% Functions 4/6
88.23% Lines 15/17

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57                    7x   7x 7x 7x   7x 7x 7x                                                   7x 7x 7x 7x 7x 7x       9x 9x    
import { onBeforeUnmount, onMounted } from 'vue'
 
// 输入态判断:焦点在 input / textarea / 可编辑区域
function isTypingTarget(e) {
  const el = e.target
  return !!el && (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.isContentEditable)
}
 
// 快捷键匹配:精确匹配(未声明的修饰符按下则不匹配)
function matches(e, s) {
  Iif (e.key.toLowerCase() !== String(s.key).toLowerCase()) return false
  // 主修饰键:声明了 mod / ctrl / meta 中任一 → 要求 Ctrl 或 Meta 按下
  const needsMod = s.mod === true || s.ctrl === true || s.meta === true
  const modPressed = e.ctrlKey || e.metaKey
  Iif (needsMod !== modPressed) return false
  // alt / shift 精确匹配(未声明即要求未按下)
  Iif (!!s.alt !== e.altKey) return false
  Iif (!!s.shift !== e.shiftKey) return false
  return true
}
 
/**
 * 全局键盘快捷键注册(可组合、可复用)。
 *
 * 用法(在任意组件 setup 顶层调用):
 *   useHotkeys([
 *     { key: 's', mod: true, ignoreWhenTyping: false, handler: save },   // Ctrl/Cmd + S
 *     { key: '/', handler: focusSearch },                                 // 单独键(无修饰符)
 *     { key: 'r', alt: true, handler: reload },                           // Alt + R
 *   ])
 *
 * 修饰符语义(精确匹配,未声明的修饰符按下则不匹配):
 *   mod    —— 主修饰键:Ctrl 或 Meta(Cmd)任一(跨平台主修饰键)
 *   ctrl / meta / alt / shift —— 单独指定(与 mod 可叠加,如 { key: 's', mod: true, shift: true })
 *
 * 其他选项:
 *   ignoreWhenTyping(默认 true)—— 焦点在 input / textarea / 可编辑区域时忽略;
 *     组合保存类快捷键(如 Ctrl+S)通常传 false,保证输入时也能保存。
 *   handler(e) —— 触发回调,默认已 preventDefault 阻止浏览器默认行为。
 *
 * 生命周期:onMounted 注册、onBeforeUnmount 卸载,组件销毁后自动解绑。
 */
export function useHotkeys(shortcuts) {
  function onKeydown(e) {
    for (const s of shortcuts) {
      Iif (!matches(e, s)) continue
      Iif (s.ignoreWhenTyping !== false && isTypingTarget(e)) continue
      e.preventDefault()
      s.handler(e)
      break
    }
  }
 
  onMounted(() => window.addEventListener('keydown', onKeydown))
  onBeforeUnmount(() => window.removeEventListener('keydown', onKeydown))
}