Members
(static, constant) EDITABLE :string
Selector for the places a person types, for the when and except options.
[contenteditable] is matched with closest, so the <b> inside an editable <div>
counts as being in one. <select> is in the list because a focused one runs its own
type-ahead on plain letters.
- Source:
Type:
-
string
Example
bindShortcut('mod+k', focusSearch, { except: EDITABLE }) // not while typing
bindShortcut('mod+b', bold, { when: '#editor' }) // only while typing, over there
(inner, constant) ALIASES
Spellings a shortcut may use for each modifier, mod excepted - that one is resolved per
platform and so cannot be a constant.
cmd and ctrl stay literal, which is the whole reason mod exists. Making them mean
"whichever this machine calls primary" reads nicer for the nine bindings out of ten that
want it, and takes away the only way to say the tenth: macOS keeps the Emacs bindings
(Ctrl+A, Ctrl+E, Ctrl+K) live inside every text field, so an editor binding literal
Control on a Mac is an ordinary thing to want and would become unsayable.
- Source:
(inner, constant) KONAMI
The konami code as ten shortcut specs, the arcade original minus Start and Select.
- Source:
(inner, constant) MODIFIERS
The four modifiers a KeyboardEvent tracks, and the whole set exclusivity is checked over.
- Source:
(inner, constant) MODIFIER_KEYS
event.key values for the modifiers themselves, which arrive as keydowns of their own.
- Source:
(inner, constant) intentWatched
Documents already watched, so repeat calls from many components cost nothing.
- Source:
(inner) keyboardIntent
Whether the last input the page saw was a key rather than a pointer.
- Source:
Methods
(static) bindSequence(specs, handler, optionsopt) → {function}
Runs a handler when a list of shortcuts is pressed in order, and returns the function that
stops it. A cheat code, or the g i pair a keyboard-first app uses to go somewhere.
Forgets itself timeout milliseconds after each step, so a half-typed sequence does not
wait around to be completed by an unrelated keypress an hour later. A wrong key restarts
it - and is then tried against the first step, so ↑ ↑ ↑ ↓ still gets to step three
rather than throwing away the ↑ that was also a fresh start.
Keydowns for the modifiers themselves pass through without disturbing the count: mod+k
as a step arrives as a keydown for Meta and then one for k, and a sequence that reset on
the first of those could never contain a shortcut with a modifier in it.
preventDefault is off here, unlike bindShortcut. A step matches long before
the sequence does, and the konami code opens on an arrow key - preventing that default
would stop the page scrolling for everyone who never finishes the sequence, which is
everyone.
- Source:
Parameters:
| Name | Type | Attributes | Description | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
specs |
Array.<string>
|
The shortcuts, in order - see |
|||||||||||||||||||||||||||||||
handler |
function
|
Called with the event that completed the sequence |
|||||||||||||||||||||||||||||||
options |
object
|
<optional> |
|
Throws:
-
If the list is empty, or a spec is malformed
- Type
-
TypeError
Returns:
- Type:
-
function
Unbind
Example
bindSequence(['g', 'i'], () => go('/inbox'))
bindSequence(['ArrowUp', 'ArrowUp', 'b', 'a'], cheat, { timeout: 2000 })
(static) bindShortcut(spec, handler, optionsopt) → {function}
Runs a handler when a shortcut is pressed, and returns the function that stops it.
Where it may fire is the point of when and except, and the reason they are selectors
rather than a boolean: the same page can want one shortcut everywhere, one only inside an
editor, and one everywhere but an editor, and all three are then a setting rather than
three code paths.
Ignores a keydown that is a key repeating unless allowRepeat says otherwise, and always
ignores one arriving mid-composition - an IME sends keydowns for the keys building a
character, and they are not the person pressing a shortcut.
The spec is parsed on the way in, so a typo throws where it was written rather than silently never firing.
- Source:
Parameters:
| Name | Type | Attributes | Description | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
spec |
string
|
Modifiers and a key, joined by |
|||||||||||||||||||||||||||||||
handler |
function
|
Called with the event |
|||||||||||||||||||||||||||||||
options |
object
|
<optional> |
|
Throws:
-
If the spec is malformed
- Type
-
TypeError
Returns:
- Type:
-
function
Unbind
Example
const unbind = bindShortcut('mod+k', () => input.focus())
bindShortcut('mod+b', bold, { when: '#editor' })
bindShortcut('/', search, { except: EDITABLE })
unbind()
(static) isKeyboardIntent() → {boolean}
Whether the last input the page saw was a key rather than a pointer.
Pointer until proven otherwise: before anyone has touched anything this is false, so
focus that arrives on load — an autofocus, a restored scroll position — is not
mistaken for someone tabbing. Requires watchInputIntent to have been called;
without it this is always false.
- Source:
- See:
-
watchInputIntent
Returns:
- Type:
-
boolean
true if the last input was a keypress
Example
watchInputIntent()
// after the person presses Tab
isKeyboardIntent() // => true
// after the person clicks
isKeyboardIntent() // => false
(static) konamiCode(handler, optionsopt) → {function}
Runs a handler on the konami code - ↑ ↑ ↓ ↓ ← → ← → B A.
The arcade original ends on Start, which no keyboard has; the web has settled on the ten
keys before it. Takes no sequence of its own: a different sequence is
bindSequence, and an option here would only be that function wearing this name.
- Source:
Parameters:
| Name | Type | Attributes | Description |
|---|---|---|---|
handler |
function
|
Called with the event that completed the sequence |
|
options |
object
|
<optional> |
As |
Returns:
- Type:
-
function
Unbind
Example
konamiCode(() => document.body.classList.add('rainbow'))
(static) matchesShortcut(event, spec) → {boolean}
Whether a keydown is the shortcut a spec describes.
The spec is modifiers and a key joined by + - mod+k, shift+alt+ArrowUp, Escape.
mod is Command on Apple platforms and Control everywhere else; cmd, ctrl, alt and
shift mean themselves. Case does not matter to the key, so mod+K and mod+k are the
same shortcut and neither demands Shift - to demand it, say mod+shift+k.
Modifiers not named must be up. mod+k does not match Ctrl+Shift+K, which is a
devtools shortcut in two browsers, and the same rule is what keeps alt+k off k. This
is the part hand-written checks get wrong, because it takes a test per modifier rather
than per modifier you thought of.
The key is matched against event.key without case or against event.code exactly, so
a layout where a modifier rewrites the character still has a way to be named: alt+k on
macOS arrives as ˚, and alt+KeyK matches it.
Says nothing about where the keydown came from - a shortcut that must not fire while
someone is typing pairs this with EDITABLE, or uses bindShortcut, which
has the option. A lone modifier (shift with no key) is not a shortcut this can express.
- Source:
Parameters:
| Name | Type | Description |
|---|---|---|
event |
KeyboardEvent
|
|
spec |
string
|
Modifiers and a key, joined by |
Throws:
-
If the spec names no key, or a modifier that is not a modifier
- Type
-
TypeError
Returns:
- Type:
-
boolean
Example
// on Windows, with Ctrl+K pressed
matchesShortcut(event, 'mod+k') // => true
// with Ctrl+Shift+K pressed
matchesShortcut(event, 'mod+k') // => false, Shift was not asked for
(static) watchInputIntent(docopt) → {void}
Starts tracking whether the person is driving the page with a keyboard or with a
pointer, so isKeyboardIntent can be asked later.
This is the part :focus-visible cannot give you. That pseudo-class matches a text
input or a contenteditable even when it was clicked into, because a browser assumes
anything taking text input wants its focus ring — right for a ring, wrong for deciding
whether to show a keyboard hint or move focus somewhere a mouse user did not ask for.
Listeners go on in capture, because a pointerdown handler somewhere in the page that
calls stopPropagation (drag implementations do, routinely) would otherwise hide the
switch to pointer. pointerdown rather than mousedown so a pen and a touch count
without waiting for emulated mouse events.
Call it once, early — before any focus you intend to judge, since the keypress that moves focus lands on whatever had focus before the element you are asking about. It is safe to call from every component that needs it: the document is only watched once. There is no unwatch; two capture listeners for the life of the page is the whole cost.
- Source:
- See:
-
isKeyboardIntent
Parameters:
| Name | Type | Attributes | Default | Description |
|---|---|---|---|---|
doc |
Document
|
<optional> |
document |
The document to watch — pass an iframe's own document to watch inside it |
Returns:
- Type:
-
void
Example
watchInputIntent()
element.addEventListener('focusin', () => {
element.classList.toggle('is-key-focus', isKeyboardIntent())
})
(inner) eventOrigin()
The element a keydown came from, reaching inside a shadow root, or null if it has none.
- Source:
(inner) inScope()
Whether a keydown is allowed to act, given a binding's when/except selectors.
- Source:
(inner) isApplePlatform()
Whether this machine is one where mod means the Command key.
Read per call rather than at import: several helpers here run in a page before anything is
known about it, and a module that touches navigator on the way in cannot be imported
under Node at all. userAgentData first because navigator.platform is deprecated;
neither is guessed at from the user-agent string, which reports AppleWebKit on machines
that are not Apple's.
- Source:
(inner) parseShortcut()
A shortcut spec split into the modifiers it demands and the key it ends on.
The key is the last +-separated part, which leaves one ambiguity worth spelling out:
mod++ is Command and the plus key, mod+ is a spec that names no key at all. They are
told apart by how many empty parts the split leaves behind - two for the first, one for
the second - and the second throws. A lookbehind would say this in one regex and is not
used: an unsupported regex literal is a SyntaxError at parse time, which takes down the
whole module rather than the one call, and this library ships to whatever browser a
consumer still supports.
An unknown modifier throws rather than resolving to nothing. Silently, mdo+k is a
shortcut that can never fire and gives no reason.
- Source: