@github/hotkey
Advanced tools
Comparing version 2.0.1 to 2.1.0
@@ -1,1 +0,7 @@ | ||
export default function hotkey(event: KeyboardEvent): string; | ||
declare const normalizedHotkeyBrand: unique symbol; | ||
export declare type NormalizedHotkeyString = string & { | ||
[normalizedHotkeyBrand]: true; | ||
}; | ||
export declare function eventToHotkeyString(event: KeyboardEvent): NormalizedHotkeyString; | ||
export declare function normalizeHotkey(hotkey: string, platform?: string | undefined): NormalizedHotkeyString; | ||
export {}; |
@@ -1,2 +0,3 @@ | ||
export default function hotkey(event) { | ||
const normalizedHotkeyBrand = Symbol('normalizedHotkey'); | ||
export function eventToHotkeyString(event) { | ||
const { ctrlKey, altKey, metaKey, key } = event; | ||
@@ -14,3 +15,3 @@ const hotkeyString = []; | ||
} | ||
const modifierKeyNames = [`Control`, 'Alt', 'Meta', 'Shift']; | ||
const modifierKeyNames = ['Control', 'Alt', 'Meta', 'Shift']; | ||
function showShift(event) { | ||
@@ -20,1 +21,23 @@ const { shiftKey, code, key } = event; | ||
} | ||
export function normalizeHotkey(hotkey, platform) { | ||
let result; | ||
result = localizeMod(hotkey, platform); | ||
result = sortModifiers(result); | ||
return result; | ||
} | ||
const matchApplePlatform = /Mac|iPod|iPhone|iPad/i; | ||
function localizeMod(hotkey, platform = navigator.platform) { | ||
const localModifier = matchApplePlatform.test(platform) ? 'Meta' : 'Control'; | ||
return hotkey.replace('Mod', localModifier); | ||
} | ||
function sortModifiers(hotkey) { | ||
const key = hotkey.split('+').pop(); | ||
const modifiers = []; | ||
for (const modifier of ['Control', 'Alt', 'Meta', 'Shift']) { | ||
if (hotkey.includes(modifier)) { | ||
modifiers.push(modifier); | ||
} | ||
} | ||
modifiers.push(key); | ||
return modifiers.join('+'); | ||
} |
@@ -1,5 +0,5 @@ | ||
import { Leaf, RadixTrie } from './radix-trie'; | ||
import eventToHotkeyString from './hotkey'; | ||
export { RadixTrie, Leaf, eventToHotkeyString }; | ||
export { eventToHotkeyString, normalizeHotkey, NormalizedHotkeyString } from './hotkey'; | ||
export { SequenceTracker, normalizeSequence, NormalizedSequenceString } from './sequence'; | ||
export { RadixTrie, Leaf } from './radix-trie'; | ||
export declare function install(element: HTMLElement, hotkey?: string): void; | ||
export declare function uninstall(element: HTMLElement): void; |
@@ -73,2 +73,85 @@ class Leaf { | ||
function eventToHotkeyString(event) { | ||
const { ctrlKey, altKey, metaKey, key } = event; | ||
const hotkeyString = []; | ||
const modifiers = [ctrlKey, altKey, metaKey, showShift(event)]; | ||
for (const [i, mod] of modifiers.entries()) { | ||
if (mod) | ||
hotkeyString.push(modifierKeyNames[i]); | ||
} | ||
if (!modifierKeyNames.includes(key)) { | ||
hotkeyString.push(key); | ||
} | ||
return hotkeyString.join('+'); | ||
} | ||
const modifierKeyNames = ['Control', 'Alt', 'Meta', 'Shift']; | ||
function showShift(event) { | ||
const { shiftKey, code, key } = event; | ||
return shiftKey && !(code.startsWith('Key') && key.toUpperCase() === key); | ||
} | ||
function normalizeHotkey(hotkey, platform) { | ||
let result; | ||
result = localizeMod(hotkey, platform); | ||
result = sortModifiers(result); | ||
return result; | ||
} | ||
const matchApplePlatform = /Mac|iPod|iPhone|iPad/i; | ||
function localizeMod(hotkey, platform = navigator.platform) { | ||
const localModifier = matchApplePlatform.test(platform) ? 'Meta' : 'Control'; | ||
return hotkey.replace('Mod', localModifier); | ||
} | ||
function sortModifiers(hotkey) { | ||
const key = hotkey.split('+').pop(); | ||
const modifiers = []; | ||
for (const modifier of ['Control', 'Alt', 'Meta', 'Shift']) { | ||
if (hotkey.includes(modifier)) { | ||
modifiers.push(modifier); | ||
} | ||
} | ||
modifiers.push(key); | ||
return modifiers.join('+'); | ||
} | ||
const SEQUENCE_DELIMITER = ' '; | ||
class SequenceTracker { | ||
constructor({ onReset } = {}) { | ||
this._path = []; | ||
this.timer = null; | ||
this.onReset = onReset; | ||
} | ||
get path() { | ||
return this._path; | ||
} | ||
get sequence() { | ||
return this._path.join(SEQUENCE_DELIMITER); | ||
} | ||
registerKeypress(event) { | ||
this._path = [...this._path, eventToHotkeyString(event)]; | ||
this.startTimer(); | ||
} | ||
reset() { | ||
var _a; | ||
this.killTimer(); | ||
this._path = []; | ||
(_a = this.onReset) === null || _a === void 0 ? void 0 : _a.call(this); | ||
} | ||
killTimer() { | ||
if (this.timer != null) { | ||
window.clearTimeout(this.timer); | ||
} | ||
this.timer = null; | ||
} | ||
startTimer() { | ||
this.killTimer(); | ||
this.timer = window.setTimeout(() => this.reset(), SequenceTracker.CHORD_TIMEOUT); | ||
} | ||
} | ||
SequenceTracker.CHORD_TIMEOUT = 1500; | ||
function normalizeSequence(sequence) { | ||
return sequence | ||
.split(SEQUENCE_DELIMITER) | ||
.map(h => normalizeHotkey(h)) | ||
.join(SEQUENCE_DELIMITER); | ||
} | ||
function isFormField(element) { | ||
@@ -113,3 +196,3 @@ if (!(element instanceof HTMLElement)) { | ||
} | ||
if (hotkey[i] === ' ') { | ||
if (hotkey[i] === SEQUENCE_DELIMITER) { | ||
acc.push(''); | ||
@@ -128,34 +211,13 @@ commaIsSeparator = false; | ||
output.push(acc); | ||
return output.map(h => h.filter(k => k !== '')).filter(h => h.length > 0); | ||
return output.map(h => h.map(k => normalizeHotkey(k)).filter(k => k !== '')).filter(h => h.length > 0); | ||
} | ||
function hotkey(event) { | ||
const { ctrlKey, altKey, metaKey, key } = event; | ||
const hotkeyString = []; | ||
const modifiers = [ctrlKey, altKey, metaKey, showShift(event)]; | ||
for (const [i, mod] of modifiers.entries()) { | ||
if (mod) | ||
hotkeyString.push(modifierKeyNames[i]); | ||
} | ||
if (!modifierKeyNames.includes(key)) { | ||
hotkeyString.push(key); | ||
} | ||
return hotkeyString.join('+'); | ||
} | ||
const modifierKeyNames = [`Control`, 'Alt', 'Meta', 'Shift']; | ||
function showShift(event) { | ||
const { shiftKey, code, key } = event; | ||
return shiftKey && !(code.startsWith('Key') && key.toUpperCase() === key); | ||
} | ||
const hotkeyRadixTrie = new RadixTrie(); | ||
const elementsLeaves = new WeakMap(); | ||
let currentTriePosition = hotkeyRadixTrie; | ||
let resetTriePositionTimer = null; | ||
let path = []; | ||
function resetTriePosition() { | ||
path = []; | ||
resetTriePositionTimer = null; | ||
currentTriePosition = hotkeyRadixTrie; | ||
} | ||
const sequenceTracker = new SequenceTracker({ | ||
onReset() { | ||
currentTriePosition = hotkeyRadixTrie; | ||
} | ||
}); | ||
function keyDownHandler(event) { | ||
@@ -173,12 +235,8 @@ if (event.defaultPrevented) | ||
} | ||
if (resetTriePositionTimer != null) { | ||
window.clearTimeout(resetTriePositionTimer); | ||
} | ||
resetTriePositionTimer = window.setTimeout(resetTriePosition, 1500); | ||
const newTriePosition = currentTriePosition.get(hotkey(event)); | ||
const newTriePosition = currentTriePosition.get(eventToHotkeyString(event)); | ||
if (!newTriePosition) { | ||
resetTriePosition(); | ||
sequenceTracker.reset(); | ||
return; | ||
} | ||
path.push(hotkey(event)); | ||
sequenceTracker.registerKeypress(event); | ||
currentTriePosition = newTriePosition; | ||
@@ -199,6 +257,6 @@ if (newTriePosition instanceof Leaf) { | ||
if (elementToFire && shouldFire) { | ||
fireDeterminedAction(elementToFire, path); | ||
fireDeterminedAction(elementToFire, sequenceTracker.path); | ||
event.preventDefault(); | ||
} | ||
resetTriePosition(); | ||
sequenceTracker.reset(); | ||
} | ||
@@ -226,2 +284,2 @@ } | ||
export { Leaf, RadixTrie, hotkey as eventToHotkeyString, install, uninstall }; | ||
export { Leaf, RadixTrie, SequenceTracker, eventToHotkeyString, install, normalizeHotkey, normalizeSequence, uninstall }; |
@@ -0,3 +1,4 @@ | ||
import { NormalizedHotkeyString } from './hotkey'; | ||
export declare function isFormField(element: Node): boolean; | ||
export declare function fireDeterminedAction(el: HTMLElement, path: string[]): void; | ||
export declare function expandHotkeyToEdges(hotkey: string): string[][]; | ||
export declare function fireDeterminedAction(el: HTMLElement, path: readonly NormalizedHotkeyString[]): void; | ||
export declare function expandHotkeyToEdges(hotkey: string): NormalizedHotkeyString[][]; |
@@ -0,1 +1,3 @@ | ||
import { normalizeHotkey } from './hotkey'; | ||
import { SEQUENCE_DELIMITER } from './sequence'; | ||
export function isFormField(element) { | ||
@@ -40,3 +42,3 @@ if (!(element instanceof HTMLElement)) { | ||
} | ||
if (hotkey[i] === ' ') { | ||
if (hotkey[i] === SEQUENCE_DELIMITER) { | ||
acc.push(''); | ||
@@ -55,3 +57,3 @@ commaIsSeparator = false; | ||
output.push(acc); | ||
return output.map(h => h.filter(k => k !== '')).filter(h => h.length > 0); | ||
return output.map(h => h.map(k => normalizeHotkey(k)).filter(k => k !== '')).filter(h => h.length > 0); | ||
} |
{ | ||
"name": "@github/hotkey", | ||
"version": "2.0.1", | ||
"version": "2.1.0", | ||
"description": "", | ||
@@ -17,3 +17,4 @@ "main": "dist/index.js", | ||
"pretest": "npm run build", | ||
"prepublishOnly": "npm run build" | ||
"prepublishOnly": "npm run build", | ||
"buildSite": "npm run build && mkdir -p pages/hotkey && cp -r dist/* pages/hotkey" | ||
}, | ||
@@ -20,0 +21,0 @@ "files": [ |
@@ -36,9 +36,16 @@ # Hotkey Behavior | ||
## Usage | ||
### HTML | ||
``` html | ||
```html | ||
<!-- Single character hotkey: triggers when "j" is pressed--> | ||
<a href="/page/2" data-hotkey="j">Next</a> | ||
<a href="/help" data-hotkey="Control+h">Help</a> | ||
<!-- Multiple hotkey aliases: triggers on both "s" and "/" --> | ||
<a href="/search" data-hotkey="s,/">Search</a> | ||
<!-- Key-sequence hotkey: triggers when "g" is pressed followed by "c"--> | ||
<a href="/rails/rails" data-hotkey="g c">Code</a> | ||
<a href="/search" data-hotkey="s,/">Search</a> | ||
<!-- Hotkey with modifiers: triggers when "Control", "Alt", and "h" are pressed at the same time --> | ||
<a href="/help" data-hotkey="Control+Alt+h">Help</a> | ||
<!-- Special "Mod" modifier localizes to "Meta" on mac, "Control" on Windows or Linux--> | ||
<a href="/settings" data-hotkey="Mod+s">Search</a> | ||
``` | ||
@@ -77,2 +84,20 @@ | ||
By default form elements (such as `input`,`textarea`,`select`) or elements with `contenteditable` will call `focus()` when the hotkey is triggered. All other elements trigger a `click()`. All elements, regardless of type, will emit a cancellable `hotkey-fire` event, so you can customize the behaviour, if you so choose: | ||
```js | ||
for (const el of document.querySelectorAll('[data-shortcut]')) { | ||
install(el, el.dataset.shortcut) | ||
if (el.matches('.frobber')) { | ||
el.addEventListener('hotkey-fire', event => { | ||
// ensure the default `focus()`/`click()` is prevented: | ||
event.preventDefault() | ||
// Use a custom behaviour instead | ||
frobulateFrobber(event.target) | ||
}) | ||
} | ||
} | ||
``` | ||
## Hotkey string format | ||
@@ -84,4 +109,8 @@ | ||
4. Multiple keys separated by a blank space represent a key sequence. For example the hotkey `g n` would activate when a user types the `g` key followed by the `n` key. | ||
5. Modifier key combos are separated with a `+` and are prepended to a key in a consistent order as follows: `Control+Alt+Meta+Shift+KEY`. | ||
6. You can use the comma key `,` as a hotkey, e.g. `a,,` would activate if the user typed `a` or `,`. `Control+,,x` would activate for `Control+,` or `x`. | ||
5. Modifier key combos are separated with a `+` and are prepended to a key in a consistent order as follows: `"Control+Alt+Meta+Shift+KEY"`. | ||
6. `"Mod"` is a special modifier that localizes to `Meta` on MacOS/iOS, and `Control` on Windows/Linux. | ||
1. `"Mod+"` can appear in any order in a hotkey string. For example: `"Mod+Alt+Shift+KEY"` | ||
2. Neither the `Control` or `Meta` modifiers should appear in a hotkey string with `Mod`. | ||
3. Due to the inconsistent lowercasing of `event.key` on Mac and iOS when `Meta` is pressed along with `Shift`, it is recommended to avoid hotkey strings containing both `Mod` and `Shift`. | ||
7. You can use the comma key `,` as a hotkey, e.g. `a,,` would activate if the user typed `a` or `,`. `Control+,,x` would activate for `Control+,` or `x`. | ||
@@ -93,3 +122,3 @@ ### Example | ||
```js | ||
"a b,Control+Alt+/" | ||
'a b,Control+Alt+/' | ||
``` | ||
@@ -96,0 +125,0 @@ |
26279
13
543
168