Skip to content

Instantly share code, notes, and snippets.

@loilo
Last active April 30, 2021 16:28
Show Gist options
  • Save loilo/2a1834dc20d842f63bd048ffbcf3dc19 to your computer and use it in GitHub Desktop.
Save loilo/2a1834dc20d842f63bd048ffbcf3dc19 to your computer and use it in GitHub Desktop.
Vue Global Keyboard Shortcut Mixin

Vue Global Keyboard Shortcut Mixin

This is a Vue.js mixin (or rather, a mixin factory) for simple keyboard shortcuts.

Features:

  • Tiny (<0.5 KB minified & gzipped)
  • Super easy to use
  • Proper restrictions (does not trigger when modifier keys don't match exactly or when an element has focus)

Note that this mixin targets modern browsers. If you need legacy browser support, you need to use a transpiler like Babel with the according polyfills.

Usage

Basic Key Press

No modifiers, just a single key press:

import shortcut from './shortcut.mjs'

new Vue({
  mixins: [
    shortcut('ArrowLeft', function() {
      // Arrow left key was pressed
    }),
    shortcut('ArrowRight', function() {
      // Arrow right key was pressed
    })
  ]
})

Note: Key names are case insensitive. That means that shortcut('A', ...) will also match key presses of a (without shift key pressed) and vice versa.

Modifier Keys

Well-known Modifiers

The modifiers you know and love from the KeyboardEvent are available: Control, Alt, Shift and Meta.

You can import them from the module and pass a bitmask of them to the shorcut function as an optional second argument.

import shortcut, { CTRL, ALT } from './shortcut.mjs'

new Vue({
  mixins: [
    shortcut('a', CTRL | ALT, function() {
      // Control + Alt + A was pressed
    })
  ]
})

OS-specific Modifiers

There are some additional modifiers available for comfort: CMD and WIN are both aliases to the META modifier, but they only apply if the user is on the according OS.

In addition, there is PRIMARY. It represents the primary modifier key of the user's operating system and aliases either CMD (on macOS) or CTRL (on other operating systems).

import shortcut, { PRIMARY } from './shortcut.mjs'

new Vue({
  mixins: [
    shortcut('s', PRIMARY, function() {
      // Matches Command + S on macOS and Control + S on other systems
    })
  ]
})

Access the Event

If you want to perform actions on the triggered KeyboardEvent, you can access the event as the first parameter to the shortcut callback:

import shortcut, { PRIMARY } from './shortcut.mjs'

new Vue({
  mixins: [
    shortcut('s', PRIMARY, function(event) {
      // Prevent the browser from saving the page
      event.preventDefault()
      
      // Perform custom save logic instead...
    })
  ]
})

Advanced Matchers

We are not restricted to passing key names as the first argument to the shortcut function. A callback function which determines whether a pressed key matches your requirements will be accepted as well:

import shortcut from './shortcut.mjs'

new Vue({
  mixins: [
    shortcut(event => /^[0-9]$/.test(event.key), function() {
      // A number key was pressed
    })
  ]
})
// Modifier keys
export const CTRL = 0b000001
export const ALT = 0b000010
export const SHIFT = 0b000100
export const META = 0b001000
// The Windows key
export const WIN = 0b010000
// The CMD key
export const CMD = 0b100000
// Check for macOS
const isMac = navigator.appVersion.includes('Macintosh')
// Determine the primary modifier key
export const PRIMARY = isMac ? CMD : CTRL
/**
* Create a mixin for simple keyboard shortcuts
*
* @param {string|string[]} matcher The key name(s) to react to
* @param {number} modifierKeys A bitmask of modifier keys
* @returns {object}
*/
export default function shortcut(matcher, ...args) {
// If only one remaining argument, treat it as callback
if (args.length === 1) {
return shortcut(matcher, 0b0000, args[0])
}
// The key the listener function will be stored at
const LISTENER = Symbol('keydown listener')
let [modifierKeys, callback] = args
// Check modifier keys for WIN or CMD
let excludedByOS = false
if (modifierKeys & (WIN | CMD)) {
// Add META to modifier keys if OS matches
if (modifierKeys & (isMac ? CMD : WIN)) {
modifierKeys = modifierKeys | META
} else {
excludedByOS = true
}
}
// Normalize matcher towards a function
if (typeof matcher === 'string') {
matcher = [matcher]
}
if (Array.isArray(matcher)) {
const lowerKeys = matcher.map(key => key.toLowerCase())
matcher = event => lowerKeys.includes(event.key.toLowerCase())
}
return {
mounted() {
this[LISTENER] = event => {
if (
// Check for exclusion by OS
!excludedByOS &&
// No explicitely focused element
document.activeElement === document.body &&
// Control key matches requirement
event.ctrlKey === Boolean(modifierKeys & CTRL) &&
// Alt key matches requirement
event.altKey === Boolean(modifierKeys & ALT) &&
// Shift key matches requirement
event.shiftKey === Boolean(modifierKeys & SHIFT) &&
// Meta key matches requirement
event.metaKey === Boolean(modifierKeys & META) &&
// Key name is the requested one
matcher(event)
) {
callback.call(this, event)
}
}
document.addEventListener('keydown', this[LISTENER])
},
destroyed() {
document.removeEventListener('keydown', this[LISTENER])
}
}
}
@LordOkami
Copy link

Thanks for sharing, but It doesn't work for me I don't know why. It's instantiating the event listener but it never call the callback 😢 . I think I'm doing something wrong...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment