Comparing version 2.0.3 to 3.0.0
/* IMPORT */ | ||
import {Shortcut, Shortcuts} from '../dist/index.js'; | ||
import Shortcuts from '../src'; | ||
/* HELPERS */ | ||
const log = document.getElementById ( 'log' ); | ||
const yep = message => { | ||
return () => { | ||
console.log ( `✅ ${message}` ); | ||
return true; | ||
}; | ||
}; | ||
function scrollBottom () { | ||
document.body.scrollTop = Number.MAX_SAFE_INTEGER; | ||
} | ||
const nope = message => { | ||
return () => { | ||
console.log ( `❌ ${message}` ); | ||
return false; | ||
}; | ||
}; | ||
function logHandler ( id, event, result ) { | ||
const handledEmoji = !result ? '✅' : '❌'; | ||
const shortcuts = id.map ( ( _, index ) => Shortcut.id2accelerator ( id.slice ( index ) ) ); | ||
log.innerHTML += `<pre>${handledEmoji} - ${event.type} - ${shortcuts.map ( s => `<kbd>${s}</kbd>` ).join ( ', ' )}</pre>`; | ||
scrollBottom (); | ||
} | ||
/* MAIN - LISTENING */ | ||
function logHandled ( shortcut ) { | ||
setTimeout ( () => { // Hacky way to run this after `logHandler` | ||
log.innerHTML += `<pre> → <kbd>${shortcut}</kbd></pre>`; | ||
scrollBottom (); | ||
}); | ||
return true; | ||
} | ||
const shortcuts = new Shortcuts (); | ||
function logRecorded ( shortcut ) { | ||
log.innerHTML += `<pre>⏺ - record - <kbd>${shortcut}</kbd></pre>`; | ||
scrollBottom (); | ||
} | ||
shortcuts.add ([ | ||
{ shortcut: '0', handler: yep ( '0' ) }, | ||
{ shortcut: '1', handler: yep ( '1' ) }, | ||
{ shortcut: '2', handler: yep ( '2' ) }, | ||
{ shortcut: '3', handler: yep ( '3' ) }, | ||
{ shortcut: '4', handler: yep ( '4' ) }, | ||
{ shortcut: '5', handler: yep ( '5' ) }, | ||
{ shortcut: '6', handler: yep ( '6' ) }, | ||
{ shortcut: '7', handler: yep ( '7' ) }, | ||
{ shortcut: '8', handler: yep ( '8' ) }, | ||
{ shortcut: '9', handler: yep ( '9' ) } | ||
]); | ||
function logClear () { | ||
log.innerHTML = ''; | ||
} | ||
shortcuts.add ([ | ||
{ shortcut: '5', handler: yep ( '5 (2)' ) }, | ||
{ shortcut: '6', handler: yep ( '6 (2)' ) }, | ||
{ shortcut: '7', handler: yep ( '7 (2)' ) }, | ||
{ shortcut: '8', handler: yep ( '8 (2)' ) }, | ||
{ shortcut: '9', handler: yep ( '9 (2)' ) } | ||
]); | ||
function patch ( shortcuts ) { | ||
const _handler = shortcuts.listener.options.handler | ||
shortcuts.listener.options.handler = function ( id, event ) { | ||
const result = _handler.call ( this, id, event ); | ||
if ( !shortcuts.recordHandler ) { | ||
logHandler ( id, event, result ); | ||
} | ||
return result; | ||
}; | ||
} | ||
shortcuts.remove ([ | ||
{ shortcut: '7', }, | ||
{ shortcut: '8', }, | ||
{ shortcut: '9', } | ||
]); | ||
/* INIT */ | ||
shortcuts.register ([ | ||
{ shortcut: 'A', handler: yep ( 'A' ) }, | ||
{ shortcut: 'B', handler: yep ( 'B' ) }, | ||
{ shortcut: 'C', handler: yep ( 'C' ) }, | ||
{ shortcut: 'D', handler: yep ( 'D' ) }, | ||
{ shortcut: 'E', handler: yep ( 'E' ) }, | ||
{ shortcut: 'F', handler: yep ( 'F' ) }, | ||
{ shortcut: 'G', handler: yep ( 'G' ) }, | ||
{ shortcut: 'H', handler: yep ( 'H' ) }, | ||
{ shortcut: 'I', handler: yep ( 'I' ) }, | ||
{ shortcut: 'J', handler: yep ( 'J' ) }, | ||
{ shortcut: 'K', handler: yep ( 'K' ) }, | ||
{ shortcut: 'L', handler: yep ( 'L' ) }, | ||
{ shortcut: 'M', handler: yep ( 'M' ) }, | ||
{ shortcut: 'N', handler: yep ( 'N' ) }, | ||
{ shortcut: 'O', handler: yep ( 'O' ) }, | ||
{ shortcut: 'P', handler: yep ( 'P' ) }, | ||
{ shortcut: 'Q', handler: yep ( 'Q' ) }, | ||
{ shortcut: 'R', handler: yep ( 'R' ) }, | ||
{ shortcut: 'S', handler: yep ( 'S' ) }, | ||
{ shortcut: 'T', handler: yep ( 'T' ) }, | ||
{ shortcut: 'U', handler: yep ( 'U' ) }, | ||
{ shortcut: 'V', handler: yep ( 'V' ) }, | ||
{ shortcut: 'W', handler: yep ( 'W' ) }, | ||
{ shortcut: 'X', handler: yep ( 'X' ) }, | ||
{ shortcut: 'Y', handler: yep ( 'Y' ) }, | ||
{ shortcut: 'Z', handler: yep ( 'Z' ) }, | ||
]); | ||
const shortcuts = new Shortcuts (); | ||
shortcuts.register ([ | ||
{ shortcut: 'F1', handler: yep ( 'F1' ) }, | ||
{ shortcut: 'F2', handler: yep ( 'F2' ) }, | ||
{ shortcut: 'F3', handler: yep ( 'F3' ) }, | ||
{ shortcut: 'F4', handler: yep ( 'F4' ) }, | ||
{ shortcut: 'F5', handler: yep ( 'F5' ) }, | ||
{ shortcut: 'F6', handler: yep ( 'F6' ) }, | ||
{ shortcut: 'F7', handler: yep ( 'F7' ) }, | ||
{ shortcut: 'F8', handler: yep ( 'F8' ) }, | ||
{ shortcut: 'F9', handler: yep ( 'F9' ) }, | ||
{ shortcut: 'F10', handler: yep ( 'F10' ) }, | ||
{ shortcut: 'F11', handler: yep ( 'F11' ) }, | ||
{ shortcut: 'F12', handler: yep ( 'F12' ) }, | ||
{ shortcut: 'F13', handler: yep ( 'F13' ) }, | ||
{ shortcut: 'F14', handler: yep ( 'F14' ) }, | ||
{ shortcut: 'F15', handler: yep ( 'F15' ) }, | ||
{ shortcut: 'F16', handler: yep ( 'F16' ) }, | ||
{ shortcut: 'F17', handler: yep ( 'F17' ) }, | ||
{ shortcut: 'F18', handler: yep ( 'F18' ) }, | ||
{ shortcut: 'F19', handler: yep ( 'F19' ) }, | ||
{ shortcut: 'F20', handler: yep ( 'F20' ) }, | ||
{ shortcut: 'F21', handler: yep ( 'F21' ) }, | ||
{ shortcut: 'F22', handler: yep ( 'F22' ) }, | ||
{ shortcut: 'F23', handler: yep ( 'F23' ) }, | ||
{ shortcut: 'F24', handler: yep ( 'F24' ) } | ||
]); | ||
patch ( shortcuts ); // For logging purposes | ||
shortcuts.register ([ | ||
{ shortcut: 'Numpad0', handler: yep ( 'Numpad0' ) }, | ||
{ shortcut: 'Numpad1', handler: yep ( 'Numpad1' ) }, | ||
{ shortcut: 'Numpad2', handler: yep ( 'Numpad2' ) }, | ||
{ shortcut: 'Numpad3', handler: yep ( 'Numpad3' ) }, | ||
{ shortcut: 'Numpad4', handler: yep ( 'Numpad4' ) }, | ||
{ shortcut: 'Numpad5', handler: yep ( 'Numpad5' ) }, | ||
{ shortcut: 'Numpad6', handler: yep ( 'Numpad6' ) }, | ||
{ shortcut: 'Numpad7', handler: yep ( 'Numpad7' ) }, | ||
{ shortcut: 'Numpad8', handler: yep ( 'Numpad8' ) }, | ||
{ shortcut: 'Numpad9', handler: yep ( 'Numpad9' ) } | ||
]); | ||
shortcuts.add ([ | ||
{ shortcut: 'A', handler: () => logHandled ( 'A' ) }, | ||
// { shortcut: 'S', handler: () => logHandled ( 'S' ) }, | ||
// { shortcut: 'CmdOrCtrl+K A', handler: () => logHandled ( 'CmdOrCtrl+K A' ) }, | ||
{ shortcut: 'Ctrl+A', handler: () => logHandled ( 'Ctrl+A' ) }, | ||
{ shortcut: 'Alt+B', handler: () => logHandled ( 'Alt+B' ) }, | ||
{ shortcut: 'Shift+C', handler: () => logHandled ( 'Shift+C' ) }, | ||
{ shortcut: 'Shift+4', handler: () => logHandled ( 'Shift+4' ) }, | ||
{ shortcut: 'Shift+%', handler: () => logHandled ( 'Shift+%' ) }, | ||
{ shortcut: 'Cmd+D', handler: () => logHandled ( 'Cmd+D' ) }, | ||
{ shortcut: 'Cmd+E', handler: () => logHandled ( 'Cmd+E' ) }, | ||
{ shortcut: 'Cmd+E', handler: () => logHandled ( 'Cmd+E (Second)' ) }, | ||
{ shortcut: 'Cmd+J', handler: () => logHandled ( 'Cmd+J' ) }, | ||
{ shortcut: 'Cmd+L', handler: () => logHandled ( 'Cmd+L' ) }, | ||
{ shortcut: '-Cmd+L' }, | ||
{ shortcut: 'Ctrl+Space', handler: () => logHandled ( 'Cmd+Space' ) }, | ||
{ shortcut: 'Cmd+Backspace', handler: () => logHandled ( 'Cmd+Backspace' ) }, | ||
{ shortcut: 'Cmd+Esc', handler: () => logHandled ( 'Cmd+Esc' ) }, | ||
{ shortcut: 'Cmd+Alt+Shift+Control+F', handler: () => logHandled ( 'Cmd+Alt+Shift+Control+F' ) }, | ||
{ shortcut: 'CmdOrCtrl+K Shift+%', handler: () => logHandled ( 'CmdOrCtrl+K Shift+%' ) }, | ||
{ shortcut: 'CmdOrCtrl+B Shift+^ CmdOrCtrl+B Shift+^', handler: () => logHandled ( 'CmdOrCtrl+B Shift+^ CmdOrCtrl+B Shift+^' ) }, | ||
{ shortcut: 'CmdOrCtrl+K CmdOrCtrl+K', handler: () => { logClear (); return logHandled ( `CmdOrCtrl+K CmdOrCtrl+K` ); } }, | ||
{ shortcut: 'Up Right Down Left', handler: () => logHandled ( 'Up Right Down Left' ) }, | ||
// { shortcut: 'Right Down', handler: () => logHandled ( 'Right Down' ) } | ||
{ shortcut: 'Ctrl Ctrl', handler: () => logHandled ( 'Ctrl Ctrl' ) }, | ||
{ shortcut: 'Alt+/', handler: () => logHandled ( 'Alt+/' ) }, | ||
{ shortcut: 'Alt', handler: () => logHandled ( 'Alt' ) } | ||
shortcuts.register ([ | ||
{ shortcut: 'NumpadAdd', handler: yep ( 'NumpadAdd' ) }, | ||
{ shortcut: 'NumpadComma', handler: yep ( 'NumpadComma' ) }, | ||
{ shortcut: 'NumpadDecimal', handler: yep ( 'NumpadDecimal' ) }, | ||
{ shortcut: 'NumpadDivide', handler: yep ( 'NumpadDivide' ) }, | ||
{ shortcut: 'NumpadEnter', handler: yep ( 'NumpadEnter' ) }, | ||
{ shortcut: 'NumpadEqual', handler: yep ( 'NumpadEqual' ) }, | ||
{ shortcut: 'NumpadMultiply', handler: yep ( 'NumpadMultiply' ) }, | ||
{ shortcut: 'NumpadSubtract', handler: yep ( 'NumpadSubtract' ) } | ||
]); | ||
shortcuts.remove ([ | ||
{ shortcut: 'Cmd+J' } | ||
shortcuts.register ([ | ||
{ shortcut: 'Left+Up', handler: nope ( 'Left+Up' ) }, | ||
{ shortcut: 'Right+Up', handler: nope ( 'Right+Up' ) }, | ||
{ shortcut: 'Left+Down', handler: nope ( 'Left+Down' ) }, | ||
{ shortcut: 'Right+Down', handler: nope ( 'Right+Down' ) }, | ||
{ shortcut: 'Left+Right', handler: nope ( 'Left+Right' ) }, | ||
{ shortcut: 'Up+Down', handler: nope ( 'Up+Down' ) } | ||
]); | ||
/* MANUAL TESTING */ | ||
shortcuts.register ([ | ||
{ shortcut: 'Down', handler: yep ( 'Down' ) }, | ||
{ shortcut: 'Left', handler: yep ( 'Left' ) }, | ||
{ shortcut: 'Right', handler: yep ( 'Right' ) }, | ||
{ shortcut: 'Up', handler: yep ( 'Up' ) } | ||
]); | ||
// Alphabet | ||
// Digit | ||
// Punctuation | ||
// Shift+Alphabet | ||
// Shift+Digit | ||
// Shift+Punctuation | ||
// Ctrl+Alphabet | ||
// Ctrl+Digit | ||
// Ctrl+Punctuation | ||
// Cmd+Alphabet | ||
// Cmd+Digit | ||
// Cmd+Punctuation | ||
// Alt+Alphabet | ||
// Alt+Digit | ||
// Alt+Punctuation | ||
// Ctrl+Shift+Alphabet | ||
// Ctrl+Shift+Digit | ||
// Ctrl+Shift+Punctuation | ||
// Cmd+Shift+Alphabet | ||
// Cmd+Shift+Digit | ||
// Cmd+Shift+Punctuation | ||
// Alt+Shift+Alphabet | ||
// Alt+Shift+Digit | ||
// Alt+Shift+Punctuation | ||
// Ctrl+Alt+Shift+Cmd+Alphabet | ||
// Ctrl+Alt+Shift+Cmd+Digit | ||
// Ctrl+Alt+Shift+Cmd+Punctuation | ||
// Alt | ||
shortcuts.register ([ | ||
{ shortcut: 'Backspace', handler: yep ( 'Backspace' ) }, | ||
{ shortcut: 'CapsLock', handler: yep ( 'CapsLock' ) }, | ||
{ shortcut: 'Delete', handler: yep ( 'Delete' ) }, | ||
{ shortcut: 'End', handler: yep ( 'End' ) }, | ||
{ shortcut: 'Enter', handler: yep ( 'Enter' ) }, | ||
{ shortcut: 'Escape', handler: yep ( 'Escape' ) }, | ||
{ shortcut: 'Home', handler: yep ( 'Home' ) }, | ||
{ shortcut: 'Insert', handler: yep ( 'Insert' ) }, | ||
{ shortcut: 'PageDown', handler: yep ( 'PageDown' ) }, | ||
{ shortcut: 'PageUp', handler: yep ( 'PageUp' ) }, | ||
{ shortcut: 'Space', handler: yep ( 'Space' ) }, | ||
{ shortcut: 'Tab', handler: yep ( 'Tab' ) } | ||
]); | ||
//FIXME: The following shortcuts make keypress misfire for some reason | ||
// Ctrl+Shift+6 keypress ?? | ||
// Ctrl+Shift+- keypress ?? | ||
// Ctrl+Shift+[ keypress ?? | ||
// Ctrl+Shift+] keypress ?? | ||
// Ctrl+Shift+\ keypress ?? | ||
shortcuts.register ([ | ||
{ shortcut: 'NumLock', handler: yep ( 'NumLock' ) }, | ||
{ shortcut: 'ScrollLock', handler: yep ( 'ScrollLock' ) } | ||
]); | ||
/* RECORD */ | ||
shortcuts.register ([ | ||
{ shortcut: 'AltLeft', handler: nope ( 'AltLeft' ) }, | ||
{ shortcut: 'AltRight', handler: nope ( 'AltRight' ) }, | ||
{ shortcut: 'Alt', handler: nope ( 'Alt' ) }, | ||
{ shortcut: 'OptionLeft', handler: nope ( 'OptionLeft' ) }, | ||
{ shortcut: 'OptionRight', handler: nope ( 'OptionRight' ) }, | ||
{ shortcut: 'Option', handler: nope ( 'Option' ) }, | ||
{ shortcut: 'CmdLeft', handler: nope ( 'CmdLeft' ) }, | ||
{ shortcut: 'CmdRight', handler: nope ( 'CmdRight' ) }, | ||
{ shortcut: 'Cmd', handler: nope ( 'Cmd' ) }, | ||
{ shortcut: 'CommandLeft', handler: nope ( 'CommandLeft' ) }, | ||
{ shortcut: 'CommandRight', handler: nope ( 'CommandRight' ) }, | ||
{ shortcut: 'Command', handler: nope ( 'Command' ) }, | ||
{ shortcut: 'MetaLeft', handler: nope ( 'MetaLeft' ) }, | ||
{ shortcut: 'MetaRight', handler: nope ( 'MetaRight' ) }, | ||
{ shortcut: 'Meta', handler: nope ( 'Meta' ) }, | ||
{ shortcut: 'CtrlLeft', handler: nope ( 'CtrlLeft' ) }, | ||
{ shortcut: 'CtrlRight', handler: nope ( 'CtrlRight' ) }, | ||
{ shortcut: 'Ctrl', handler: nope ( 'Ctrl' ) }, | ||
{ shortcut: 'ControlLeft', handler: nope ( 'ControlLeft' ) }, | ||
{ shortcut: 'ControlRight', handler: nope ( 'ControlRight' ) }, | ||
{ shortcut: 'Control', handler: nope ( 'Control' ) }, | ||
{ shortcut: 'ShiftLeft', handler: nope ( 'ShiftLeft' ) }, | ||
{ shortcut: 'ShiftRight', handler: nope ( 'ShiftRight' ) }, | ||
{ shortcut: 'Shift', handler: nope ( 'Shift' ) }, | ||
{ shortcut: 'CmdLeftOrCtrlLeft', handler: nope ( 'CmdLeftOrCtrlLeft' ) }, | ||
{ shortcut: 'CmdRightOrCtrlRight', handler: nope ( 'CmdRightOrCtrlRight' ) }, | ||
{ shortcut: 'CmdOrCtrl', handler: nope ( 'CmdOrCtrl' ) }, | ||
{ shortcut: 'CommandLeftOrControlLeft', handler: nope ( 'CommandLeftOrControlLeft' ) }, | ||
{ shortcut: 'CommandRightOrControlRight', handler: nope ( 'CommandRightOrControlRight' ) }, | ||
{ shortcut: 'CommandOrControl', handler: nope ( 'CommandOrControl' ) } | ||
]); | ||
let recordDispose; | ||
shortcuts.register ([ | ||
{ shortcut: '!', handler: nope ( 'ExclationMark' ) }, | ||
{ shortcut: '"', handler: nope ( 'DoubleQuote' ) }, | ||
{ shortcut: '#', handler: nope ( 'Hash' ) }, | ||
{ shortcut: '$', handler: nope ( 'Dollar' ) }, | ||
{ shortcut: '%', handler: nope ( 'Percent' ) }, | ||
{ shortcut: '&', handler: nope ( 'Ampersand' ) }, | ||
{ shortcut: '\'', handler: nope ( 'Quote' ) }, | ||
{ shortcut: '(', handler: nope ( 'ParenthesisLeft' ) }, | ||
{ shortcut: ')', handler: nope ( 'ParenthesisRight' ) }, | ||
{ shortcut: '*', handler: nope ( 'Asterisk' ) }, | ||
{ shortcut: '+', handler: nope ( 'Plus' ) }, | ||
{ shortcut: ',', handler: nope ( 'Comma' ) }, | ||
{ shortcut: '-', handler: nope ( 'Minus' ) }, | ||
{ shortcut: '.', handler: nope ( 'Period' ) }, | ||
{ shortcut: '/', handler: nope ( 'Slash' ) }, | ||
{ shortcut: ':', handler: nope ( 'Colon' ) }, | ||
{ shortcut: ';', handler: nope ( 'Semicolon' ) }, | ||
{ shortcut: '<', handler: nope ( 'LessThan' ) }, | ||
{ shortcut: '=', handler: nope ( 'Equal' ) }, | ||
{ shortcut: '>', handler: nope ( 'GreaterThan' ) }, | ||
{ shortcut: '?', handler: nope ( 'QuestionMark' ) }, | ||
{ shortcut: '@', handler: nope ( 'At' ) }, | ||
{ shortcut: '[', handler: nope ( 'BracketLeft' ) }, | ||
{ shortcut: '\\', handler: nope ( 'Backslash' ) }, | ||
{ shortcut: ']', handler: nope ( 'BracketRight' ) }, | ||
{ shortcut: '^', handler: nope ( 'Caret' ) }, | ||
{ shortcut: '_', handler: nope ( 'Underscore' ) }, | ||
{ shortcut: '`', handler: nope ( 'Backquote' ) }, | ||
{ shortcut: '{', handler: nope ( 'BraceLeft' ) }, | ||
{ shortcut: '|', handler: nope ( 'Pipe' ) }, | ||
{ shortcut: '}', handler: nope ( 'BraceRight' ) }, | ||
{ shortcut: '~', handler: nope ( 'Tilde' ) } | ||
]); | ||
window.record = () => { | ||
window.unrecord (); | ||
recordDispose = shortcuts.record ( logRecorded ); | ||
}; | ||
shortcuts.register ([ | ||
{ shortcut: 'Shift+Plus', handler: nope ( 'Shift+Plus (Alias)' ) }, | ||
{ shortcut: 'Shift+!', handler: nope ( 'Shift+ExclationMark' ) }, | ||
{ shortcut: 'Shift+"', handler: nope ( 'Shift+DoubleQuote' ) }, | ||
{ shortcut: 'Shift+#', handler: nope ( 'Shift+Hash' ) }, | ||
{ shortcut: 'Shift+$', handler: nope ( 'Shift+Dollar' ) }, | ||
{ shortcut: 'Shift+%', handler: nope ( 'Shift+Percent' ) }, | ||
{ shortcut: 'Shift+&', handler: nope ( 'Shift+Ampersand' ) }, | ||
{ shortcut: 'Shift+\'', handler: nope ( 'Shift+Quote' ) }, | ||
{ shortcut: 'Shift+(', handler: nope ( 'Shift+ParenthesisLeft' ) }, | ||
{ shortcut: 'Shift+)', handler: nope ( 'Shift+ParenthesisRight' ) }, | ||
{ shortcut: 'Shift+*', handler: nope ( 'Shift+Asterisk' ) }, | ||
{ shortcut: 'Shift++', handler: nope ( 'Shift+Plus' ) }, | ||
{ shortcut: 'Shift+,', handler: nope ( 'Shift+Comma' ) }, | ||
{ shortcut: 'Shift+-', handler: nope ( 'Shift+Minus' ) }, | ||
{ shortcut: 'Shift+.', handler: nope ( 'Shift+Period' ) }, | ||
{ shortcut: 'Shift+/', handler: nope ( 'Shift+Slash' ) }, | ||
{ shortcut: 'Shift+:', handler: nope ( 'Shift+Colon' ) }, | ||
{ shortcut: 'Shift+;', handler: nope ( 'Shift+Semicolon' ) }, | ||
{ shortcut: 'Shift+<', handler: nope ( 'Shift+LessThan' ) }, | ||
{ shortcut: 'Shift+=', handler: nope ( 'Shift+Equal' ) }, | ||
{ shortcut: 'Shift+>', handler: nope ( 'Shift+GreaterThan' ) }, | ||
{ shortcut: 'Shift+?', handler: nope ( 'Shift+QuestionMark' ) }, | ||
{ shortcut: 'Shift+@', handler: nope ( 'Shift+At' ) }, | ||
{ shortcut: 'Shift+[', handler: nope ( 'Shift+BracketLeft' ) }, | ||
{ shortcut: 'Shift+\\', handler: nope ( 'Shift+Backslash' ) }, | ||
{ shortcut: 'Shift+]', handler: nope ( 'Shift+BracketRight' ) }, | ||
{ shortcut: 'Shift+^', handler: nope ( 'Shift+Caret' ) }, | ||
{ shortcut: 'Shift+_', handler: nope ( 'Shift+Underscore' ) }, | ||
{ shortcut: 'Shift+`', handler: nope ( 'Shift+Backquote' ) }, | ||
{ shortcut: 'Shift+{', handler: nope ( 'Shift+BraceLeft' ) }, | ||
{ shortcut: 'Shift+|', handler: nope ( 'Shift+Pipe' ) }, | ||
{ shortcut: 'Shift+}', handler: nope ( 'Shift+BraceRight' ) }, | ||
{ shortcut: 'Shift+~', handler: nope ( 'Shift+Tilde' ) } | ||
]); | ||
window.unrecord = () => { | ||
if ( recordDispose ) recordDispose (); | ||
}; | ||
shortcuts.register ([ | ||
{ shortcut: 'Shift+1', handler: nope ( 'Shift+1' ) }, | ||
{ shortcut: 'Shift+2', handler: nope ( 'Shift+2' ) }, | ||
{ shortcut: 'Shift+3', handler: nope ( 'Shift+3' ) }, | ||
{ shortcut: 'Shift+4', handler: nope ( 'Shift+4' ) }, | ||
{ shortcut: 'Shift+5', handler: nope ( 'Shift+5' ) }, | ||
{ shortcut: 'Shift+6', handler: nope ( 'Shift+6' ) }, | ||
{ shortcut: 'Shift+7', handler: nope ( 'Shift+7' ) }, | ||
{ shortcut: 'Shift+8', handler: nope ( 'Shift+8' ) }, | ||
{ shortcut: 'Shift+9', handler: nope ( 'Shift+9' ) }, | ||
{ shortcut: 'Shift+0', handler: nope ( 'Shift+0' ) } | ||
]); | ||
shortcuts.register ([ | ||
{ shortcut: 'Shift+A', handler: nope ( 'Shift+A' ) }, | ||
{ shortcut: 'Shift+B', handler: nope ( 'Shift+B' ) }, | ||
{ shortcut: 'Shift+C', handler: nope ( 'Shift+C' ) }, | ||
{ shortcut: 'Shift+Z', handler: nope ( 'Shift+Z' ) }, | ||
{ shortcut: 'Shift+Y', handler: nope ( 'Shift+Y' ) }, | ||
{ shortcut: 'Shift+X', handler: nope ( 'Shift+X' ) } | ||
]); | ||
shortcuts.register ([ | ||
{ shortcut: 'ClickLeft', handler: nope ( 'ClickLeft' ) }, | ||
{ shortcut: 'ClickRight', handler: nope ( 'ClickRight' ) }, | ||
{ shortcut: 'ClickMiddle', handler: nope ( 'ClickMiddle' ) }, | ||
{ shortcut: 'MouseLeft', handler: nope ( 'MouseLeft' ) }, | ||
{ shortcut: 'MouseRight', handler: nope ( 'MouseRight' ) }, | ||
{ shortcut: 'MouseMiddle', handler: nope ( 'MouseMiddle' ) }, | ||
{ shortcut: 'Mouse0', handler: nope ( 'Mouse0' ) }, | ||
{ shortcut: 'Mouse1', handler: nope ( 'Mouse1' ) }, | ||
{ shortcut: 'Mouse2', handler: nope ( 'Mouse2' ) }, | ||
{ shortcut: 'Mouse3', handler: nope ( 'Mouse3' ) }, | ||
{ shortcut: 'Mouse4', handler: nope ( 'Mouse4' ) }, | ||
{ shortcut: 'Mouse5', handler: nope ( 'Mouse5' ) }, | ||
{ shortcut: 'Mouse6', handler: nope ( 'Mouse6' ) }, | ||
{ shortcut: 'Mouse7', handler: nope ( 'Mouse7' ) }, | ||
{ shortcut: 'Mouse8', handler: nope ( 'Mouse8' ) }, | ||
{ shortcut: 'Mouse9', handler: nope ( 'Mouse9' ) } | ||
]); | ||
shortcuts.register ([ | ||
{ shortcut: 'Alt+F', handler: yep ( 'Alt+F' ) }, | ||
{ shortcut: 'Cmd+K Cmd+A', handler: yep ( 'Cmd+K Cmd+A' ) }, | ||
{ shortcut: 'Cmd+K B', handler: yep ( 'Cmd+K B' ) }, | ||
{ shortcut: 'Cmd+K Alt+C', handler: yep ( 'Cmd+K Alt+C' ) }, | ||
{ shortcut: 'Shift+ClickLeft', handler: yep ( 'Shift+ClickLeft' ) }, | ||
{ shortcut: 'Cmd+ClickMiddle', handler: yep ( 'Cmd+ClickMiddle' ) }, | ||
{ shortcut: 'Alt+ClickLeft Cmd+ClickMiddle', handler: yep ( 'Alt+ClickLeft Cmd+ClickMiddle' ) } | ||
]); | ||
shortcuts.register ([ | ||
{ shortcut: 'CmdLeft+CmdRight', handler: yep ( 'CmdLeft+CmdRight' ) }, | ||
// { shortcut: 'CmdLeft+CmdRight AltLeft+AltRight', handler: yep ( 'CmdLeft+CmdRight AltLeft+AltRight' ) } //TODO It'd be cool to support this too | ||
]); | ||
// shortcuts.register ( 'Up Up Down Down Left Right Left Right B A', yep ( 'Up Up Down Down Left Right Left Right B A 🚀' ), { konami: true } ); //TODO Maybe support this too | ||
shortcuts.register ([ | ||
{ shortcut: 'Left+Up', handler: nope ( 'Left+Up' ) }, | ||
{ shortcut: 'Left+Down', handler: nope ( 'Left+Down' ) }, | ||
{ shortcut: 'Left+Right', handler: nope ( 'Left+Right' ) }, | ||
{ shortcut: 'Up+Down', handler: nope ( 'Up+Down' ) }, | ||
{ shortcut: 'Up+Right', handler: nope ( 'Up+Right' ) }, | ||
{ shortcut: 'Down+Right', handler: nope ( 'Down+Right' ) } | ||
]); | ||
shortcuts.register ( { shortcut: 'Ctrl+Cmd+Right', handler: yep ( 'Ctrl+Cmd+Right' ) } ); | ||
shortcuts.register ([ | ||
{ shortcut: '§', handler: yep ( '§' ) }, | ||
{ shortcut: '±', handler: yep ( '±' ) }, | ||
{ shortcut: 'Shift+§', handler: nope ( 'Shift+§' ) }, | ||
{ shortcut: 'Shift+±', handler: nope ( 'Shift+±' ) } | ||
]); | ||
shortcuts.register ( { shortcut: 'Ctrl+Cmd+A', handler: nope ( 'NOT DISPOSED!' ) } )(); | ||
shortcuts.start (); | ||
// setTimeout ( () => { | ||
// shortcuts.stop (); | ||
// shortcuts.reset (); | ||
// }, 5000 ); | ||
/* EXPORT */ | ||
globalThis.shortcuts = shortcuts; | ||
globalThis.S = shortcuts; |
@@ -1,13 +0,2 @@ | ||
declare const MODIFIER_KEY_BITMASK = 65280; | ||
declare const TRIGGER_KEY_BITMASK = 255; | ||
declare const PLUSES_RE: RegExp; | ||
declare const WHITESPACE_RE: RegExp; | ||
declare const SHORTCUT_RE: RegExp; | ||
declare const RESULT: { | ||
readonly HANDLED: 0; | ||
readonly UNHANDLED: 1; | ||
readonly UNHANDLEABLE: 2; | ||
}; | ||
export { MODIFIER_KEY_BITMASK, TRIGGER_KEY_BITMASK }; | ||
export { PLUSES_RE, WHITESPACE_RE, SHORTCUT_RE }; | ||
export { RESULT }; | ||
declare const FORMAT = "long-inflexible-directional"; | ||
export { FORMAT }; |
/* MAIN */ | ||
const MODIFIER_KEY_BITMASK = 65280; // Bitmask that includes all modifier keys and none of the triggers | ||
const TRIGGER_KEY_BITMASK = 0b11111111; // Bitmask that includes all trigger keys and none of the modifiers | ||
const PLUSES_RE = /\+{2,}/gi; | ||
const WHITESPACE_RE = /\s+/gi; | ||
const SHORTCUT_RE = /^\s*?(?:(?:^-?|\s|\+)(?:alt|option|cmd|command|meta|ctrl|control|shift|cmdorctrl|commandorcontrol|backspace|capslock|del|delete|down|end|enter|return|esc|escape|home|insert|left|pagedown|pageup|right|space|spacebar|tab|up|plus|\d|[a-z]|f(?:\d|1\d|2[0-4])|numpad\d|[!"#$%&'()*+,./:;<=>?@[\]^_`{|}~-]))+\s*$/i; // Regex that matches a shortcut | ||
const RESULT = { | ||
HANDLED: 0, | ||
UNHANDLED: 1, | ||
UNHANDLEABLE: 2 // No handler caught that, and there are no deeper handlers that could catch that | ||
}; | ||
const FORMAT = 'long-inflexible-directional'; | ||
/* EXPORT */ | ||
export { MODIFIER_KEY_BITMASK, TRIGGER_KEY_BITMASK }; | ||
export { PLUSES_RE, WHITESPACE_RE, SHORTCUT_RE }; | ||
export { RESULT }; | ||
export { FORMAT }; |
@@ -1,3 +0,16 @@ | ||
import Shortcut from './shortcut'; | ||
import Shortcuts from './shortcuts'; | ||
export { Shortcut, Shortcuts }; | ||
import type { Descriptor, Disposer, Handler, Options, Registration } from './types'; | ||
declare class Shortcuts { | ||
private registrations; | ||
private shosho; | ||
constructor(options: Options); | ||
get: () => Descriptor[]; | ||
add: (descriptor: Descriptor | Descriptor[]) => void; | ||
register: (descriptor: Descriptor | Descriptor[]) => Disposer; | ||
remove: (descriptor: Descriptor | Descriptor[]) => void; | ||
reset: () => void; | ||
trigger: (shortcut: string) => boolean; | ||
start: () => void; | ||
stop: () => void; | ||
} | ||
export default Shortcuts; | ||
export type { Descriptor, Disposer, Handler, Options, Registration }; |
/* IMPORT */ | ||
import Shortcut from './shortcut.js'; | ||
import Shortcuts from './shortcuts.js'; | ||
import ShoSho from 'shosho'; | ||
import { FORMAT } from './constants.js'; | ||
import { castArray } from './utils.js'; | ||
/* MAIN */ | ||
class Shortcuts { | ||
/* CONSTRUCTOR */ | ||
constructor(options) { | ||
/* VARIABLES */ | ||
this.registrations = []; | ||
/* PUBLIC API */ | ||
this.get = () => { | ||
return this.registrations.map(registration => registration.descriptor); | ||
}; | ||
this.add = (descriptor) => { | ||
const descriptors = castArray(descriptor); | ||
for (const descriptor of descriptors) { | ||
if (descriptor.shortcut.startsWith('-')) { | ||
this.remove(descriptor); | ||
} | ||
else if (descriptor.handler) { | ||
const dispose = this.shosho.register(descriptor.shortcut, descriptor.handler); | ||
const shortcut = ShoSho.format(descriptor.shortcut, FORMAT); | ||
const registration = { descriptor, dispose, shortcut }; | ||
this.registrations.push(registration); | ||
} | ||
else { | ||
console.error(`Cannot register shortcut "${descriptor.shortcut}", which has no handler`); | ||
} | ||
} | ||
}; | ||
this.register = (descriptor) => { | ||
this.add(descriptor); | ||
return () => { | ||
this.remove(descriptor); | ||
}; | ||
}; | ||
this.remove = (descriptor) => { | ||
const descriptors = castArray(descriptor); | ||
for (const descriptor of descriptors) { | ||
const handler = descriptor.handler; | ||
const shortcut = ShoSho.format(descriptor.shortcut.replace(/^-/, ''), FORMAT); | ||
this.registrations = this.registrations.filter(registration => { | ||
if ((registration.shortcut !== shortcut) || (handler && registration.descriptor.handler !== handler)) { | ||
return true; | ||
} | ||
else { | ||
registration.dispose(); | ||
return false; | ||
} | ||
}); | ||
} | ||
}; | ||
this.reset = () => { | ||
this.registrations = []; | ||
this.shosho.reset(); | ||
}; | ||
this.trigger = (shortcut) => { | ||
return this.shosho.trigger(shortcut); | ||
}; | ||
this.start = () => { | ||
this.shosho.start(); | ||
}; | ||
this.stop = () => { | ||
this.shosho.stop(); | ||
}; | ||
this.registrations = []; | ||
this.shosho = new ShoSho(options); | ||
} | ||
} | ||
/* EXPORT */ | ||
export { Shortcut, Shortcuts }; | ||
export default Shortcuts; |
@@ -1,33 +0,17 @@ | ||
declare type Chord = string; | ||
declare type ChordID = number; | ||
declare type Shortcut = string; | ||
declare type ShortcutID = ChordID[]; | ||
declare type Disposer = () => void; | ||
declare type RecordHandler = (shortcut: Shortcut) => void; | ||
declare type ShouldHandleEventFunction = (event: KeyboardEvent) => boolean; | ||
declare type ListenerOptions = { | ||
capture?: boolean; | ||
target?: Node; | ||
handler: (id: ShortcutID, event: KeyboardEvent) => 0 | 1 | 2 | ShortcutID; | ||
shouldHandleEvent?: ShouldHandleEventFunction; | ||
import type { Options } from 'shosho'; | ||
type Descriptor = { | ||
handler?: Handler; | ||
shortcut: string; | ||
}; | ||
declare type ShortcutsOptions = { | ||
capture?: boolean; | ||
target?: Node; | ||
shortcuts?: ShortcutDescriptor[]; | ||
shouldHandleEvent?: ShouldHandleEventFunction; | ||
type Disposer = { | ||
(): void; | ||
}; | ||
declare type ShortcutsTree = { | ||
[id: number]: ShortcutsTree; | ||
parent?: ShortcutsTree; | ||
id?: ChordID; | ||
size: number; | ||
handlers: Function[]; | ||
type Handler = { | ||
(event?: Event): boolean | void; | ||
}; | ||
declare type ShortcutDescriptor = { | ||
handler?: (event: KeyboardEvent) => boolean | void; | ||
type Registration = { | ||
descriptor: Descriptor; | ||
dispose: Disposer; | ||
shortcut: string; | ||
}; | ||
export type { Chord, ChordID, Shortcut, ShortcutID }; | ||
export type { Disposer, RecordHandler, ShouldHandleEventFunction }; | ||
export type { ListenerOptions, ShortcutsOptions, ShortcutsTree, ShortcutDescriptor }; | ||
export type { Descriptor, Disposer, Handler, Options, Registration }; |
@@ -1,2 +0,2 @@ | ||
/* MAIN */ | ||
/* IMPORT */ | ||
export {}; |
@@ -1,10 +0,2 @@ | ||
declare const Utils: { | ||
isArray: (value: unknown) => value is unknown[]; | ||
isEqual: (a: number[], b: number[]) => boolean; | ||
isFalsy: (value: unknown) => boolean; | ||
isKeyboardEvent: (event: Event) => event is KeyboardEvent; | ||
isMac: () => boolean; | ||
isTruthy: (value: unknown) => boolean; | ||
memoize: <T, R>(fn: (arg: T) => R) => (arg: T) => R; | ||
}; | ||
export default Utils; | ||
declare const castArray: <T>(value: T | T[]) => T[]; | ||
export { castArray }; |
/* MAIN */ | ||
const Utils = { | ||
/* API */ | ||
isArray: (value) => { | ||
return Array.isArray(value); | ||
}, | ||
isEqual: (a, b) => { | ||
if (a.length !== b.length) | ||
return false; | ||
for (let i = 0, l = a.length; i < l; i++) { | ||
if (a[i] !== b[i]) | ||
return false; | ||
} | ||
return true; | ||
}, | ||
isFalsy: (value) => { | ||
return !value; | ||
}, | ||
isKeyboardEvent: (event) => { | ||
return event.type.startsWith('key'); | ||
}, | ||
isMac: () => { | ||
if (typeof navigator !== 'object') | ||
return false; | ||
return /mac|ipod|iphone|ipad/i.test(navigator.platform); | ||
}, | ||
isTruthy: (value) => { | ||
return !!value; | ||
}, | ||
memoize: (fn) => { | ||
const cache = new Map(); | ||
return (arg) => { | ||
const cached = cache.get(arg); | ||
if (cached || cache.has(arg)) | ||
return cached; //TSC | ||
const result = fn(arg); | ||
cache.set(arg, result); | ||
return result; | ||
}; | ||
} | ||
const castArray = (value) => { | ||
return Array.isArray(value) ? value : [value]; | ||
}; | ||
/* EXPORT */ | ||
export default Utils; | ||
export { castArray }; |
@@ -5,41 +5,13 @@ { | ||
"description": "Super performant and feature rich shortcuts management library.", | ||
"version": "2.0.3", | ||
"version": "3.0.0", | ||
"type": "module", | ||
"main": "dist/index.js", | ||
"exports": { | ||
".": { | ||
"import": "./dist/index.js", | ||
"types": "./dist/index.d.ts" | ||
}, | ||
"./constants": { | ||
"import": "./dist/constants.js", | ||
"types": "./dist/constants.d.ts" | ||
}, | ||
"./maps": { | ||
"import": "./dist/maps.js", | ||
"types": "./dist/maps.d.ts" | ||
} | ||
}, | ||
"typesVersions": { | ||
"*": { | ||
"constants": [ | ||
"./dist/constants.d.ts" | ||
], | ||
"maps": [ | ||
"./dist/maps.d.ts" | ||
] | ||
} | ||
}, | ||
"exports": "./dist/index.js", | ||
"types": "./dist/index.d.ts", | ||
"scripts": { | ||
"benchmark": "tsex benchmark", | ||
"benchmark:watch": "tsex benchmark --watch", | ||
"clean": "tsex clean", | ||
"compile": "tsex compile", | ||
"compile:watch": "tsex compile --watch", | ||
"test": "tsex test", | ||
"test:watch": "tsex test --watch", | ||
"demo:build": "cd demo && mkdir -p dist && esbuild --bundle index.js > ./dist/bundle.js", | ||
"demo:build:watch": "cd demo && mkdir -p dist && esbuild --watch --bundle index.js > ./dist/bundle.js", | ||
"demo:serve": "cd demo && open index.html", | ||
"prepublishOnly": "npm run clean && npm run compile && npm run test" | ||
"demo": "vite demo", | ||
"prepublishOnly": "tsex prepare" | ||
}, | ||
@@ -50,10 +22,10 @@ "keywords": [ | ||
], | ||
"dependencies": { | ||
"shosho": "^1.4.0" | ||
}, | ||
"devDependencies": { | ||
"benchloop": "^1.3.2", | ||
"call-spy": "^3.0.0", | ||
"esbuild": "^0.15.13", | ||
"fava": "^0.0.6", | ||
"tsex": "^1.1.2", | ||
"typescript": "^4.8.4" | ||
"tsex": "^2.2.4", | ||
"typescript": "^4.9.5", | ||
"vite": "^4.3.9" | ||
} | ||
} |
157
readme.md
@@ -5,9 +5,4 @@ # Shortcuts | ||
## Features | ||
This library is just a thin wrapper over [`ShoSho`](https://github.com/fabiospampinato/shosho) that provides a VSCode-like interface for managing shortcuts. Definitely check out [`ShoSho`](https://github.com/fabiospampinato/shosho), which provides more features and it may offer a lower-level but maybe more convenient API for your use case. | ||
- **Super performant**: it's about as fast as it gets, I don't think it's possible to do significantly better than this. | ||
- **Sequences support**: sequences (a.k.a. konami codes), are supported too, so you can bind actions to <kbd>Up Right Down Left</kbd> or whatever shortcuts sequence you want. | ||
- **Shortcut to Accelerator**: supported shortcuts can be converted to their [Electron's Accelerator](https://electronjs.org/docs/api/accelerator) equivalent. | ||
- **Shortcut to symbols**: supported shortcuts can be converted to symbols (e.g. `⌘A`), this is useful for providing shortcut hints in tooltips. | ||
## Install | ||
@@ -21,111 +16,93 @@ | ||
This library provides a [`Shortcuts`](#shortcuts-class) class, which will manage your [shortcuts](#shortcut-syntax), and a [`Shortcut`](#shortcut-object) object, which provides some utilities. | ||
The following interface is provided: | ||
### Shortcut Syntax | ||
```ts | ||
type Descriptor = { | ||
handler?: ( event?: Event ): boolean | void, | ||
shortcut: string | ||
}; | ||
The following keys can be used when defining a shortcut: | ||
type Disposer = { | ||
(): void | ||
}; | ||
- **Modifiers**: <kbd>Alt</kbd>/<kbd>Option</kbd>, <kbd>Cmd</kbd>/<kbd>Command</kbd>/<kbd>Meta</kbd>, <kbd>Ctrl</kbd>/<kbd>Control</kbd>, <kbd>Shift</kbd>, <kbd>CmdOrCtrl</kbd>/<kbd>CommandOrControl</kbd>. | ||
- **Digits**: <kbd>1-9</kbd>. | ||
- **Alphabet letters**: <kbd>A-Z</kbd>. | ||
- **Function keys**: <kbd>F1-F24</kbd>. | ||
- **Numpad digits**: <kbd>Numpad0-Numpad9</kbd>. | ||
- **Special keys**: <kbd>Backspace</kbd>, <kbd>Capslock</kbd>, <kbd>Del</kbd>/<kbd>Delete</kbd>, <kbd>Down</kbd>, <kbd>End</kbd>, <kbd>Enter</kbd>/<kbd>Return</kbd>, <kbd>Esc</kbd>/<kbd>Escape</kbd>, <kbd>Home</kbd>, <kbd>Insert</kbd>, <kbd>Left</kbd>, <kbd>PageDown</kbd>, <kbd>PageUp</kbd>, <kbd>Right</kbd>, <kbd>Space</kbd>/<kbd>Spacebar</kbd>, <kbd>Tab</kbd>, <kbd>Up</kbd>. | ||
- **Punctuation keys**: <kbd>!</kbd>, <kbd>"</kbd>, <kbd>#</kbd>, <kbd>$</kbd>, <kbd>%</kbd>, <kbd>&</kbd>, <kbd>'</kbd>, <kbd>(</kbd>, <kbd>)</kbd>, <kbd>*</kbd>, <kbd>+</kbd>/<kbd>plus</kbd>, <kbd>,</kbd>, <kbd>-</kbd>, <kbd>.</kbd>, <kbd>/</kbd>, <kbd>:</kbd>, <kbd>;</kbd>, <kbd><</kbd>, <kbd>=</kbd>, <kbd>></kbd>, <kbd>?</kbd>, <kbd>@</kbd>, <kbd>[</kbd>, <kbd>\\</kbd>, <kbd>]</kbd>, <kbd>^</kbd>, <kbd>_</kbd>, <kbd>`</kbd>, <kbd>{</kbd>, <kbd>|</kbd>, <kbd>}</kbd>, <kbd>~</kbd>. | ||
type Options = { | ||
capture?: boolean, | ||
target?: Window | Document | HTMLElement | SVGElement | MathMLElement, | ||
shouldHandleEvent?: ( event: Event ) => boolean | ||
}; | ||
Other keys are not supported. | ||
- ℹ️ Shortcuts are case insensitive. | ||
- ℹ️ Keys in a single shortcut must be joined by a plus sign (e.g. <kbd>Ctrl+A</kbd>). | ||
- ℹ️ Sequences of shortcuts must be joined by a space (e.g. <kbd>Ctrl+K Ctrl+B</kbd>). | ||
### Shortcuts Class | ||
The Shortcuts class will be used for adding/removing/resetting/recording shortcuts. This is its interface: | ||
```ts | ||
class Shortcuts { | ||
constructor ( options?: { shortcuts?: ShortcutDescriptor[]: capture?: boolean, target?: Node, shouldHandleEvent?: event => boolean } ); | ||
get (): ShortcutDescriptor[]; | ||
add ( descriptors: ShortcutDescriptor | ShortcutDescriptor[] ); | ||
remove ( descriptors: ShortcutDescriptor | ShortcutDescriptor[] ); | ||
reset (); | ||
record ( handler: ( shortcut ) => any ): Function; | ||
constructor ( options: Options ); | ||
get (): Descriptor[]; | ||
add ( descriptor: Descriptor | Descriptor[] ): void; | ||
register ( descriptor: Descriptor | Descriptor[] ): Disposer; | ||
remove ( descriptor: Descriptor | Descriptor[] ): void; | ||
reset (): void; | ||
trigger ( shortcut: string ): boolean; | ||
start (): void; | ||
stop (): void; | ||
} | ||
``` | ||
- ℹ️ The `shortcuts` option accepts an optional array of shortcuts descriptors. More on this below. | ||
- ℹ️ The `capture` option governs whether events are attached for the capturing phase or for the bubbling phase of the propagation. | ||
- ℹ️ The `target` option accepts an optional DOM node, where the keyboard evenr listener will be attached to. | ||
- ℹ️ The `shouldHandleEvent` option accepts an optional function which will be used for determining, for each keyboard event, if it should be handled by this library. By default that function is: `event => !event.defaultPrevented`. | ||
This is how you'd use the library: | ||
A shortcut descriptor looks like this: | ||
```ts | ||
{ | ||
handler?: ( event: KeyboardEvent ) => boolean | void, | ||
shortcut: string | ||
} | ||
``` | ||
import {Shortcuts} from 'shortcuts'; | ||
Usage: | ||
// Let's create a new shortcuts manager instance | ||
```ts | ||
import {Shortcuts} from 'shortcuts'; | ||
const shortcuts = new Shortcuts ({ | ||
capture: true, // Handle events during the capturing phase | ||
target: document, // Listening for events on the document object | ||
shouldHandleEvent ( event ) { | ||
return true; // Handle all possible events | ||
} | ||
}); | ||
const shortcuts = new Shortcuts (); | ||
// Let's register and unregister some shortcuts | ||
// Handlers are filtered by shortcut and executed from bottom to top, stopping at the first handler that returns true | ||
function CtrlBHandler () {}; | ||
const onCtrlB = () => {}; | ||
shortcuts.add ([ // Adding some shortcuts | ||
{ shortcut: 'Ctrl+A', handler: event => { | ||
console.log ( event ); | ||
return true; // Returning true because we don't want other handlers for the same shortcut to be called later | ||
}}, | ||
{ shortcut: 'Ctrl+B', handler: CtrlBHandler }, | ||
{ shortcut: 'CmdOrCtrl+K Shift+B', handler: () => { | ||
// Doing something... | ||
return true; // Returning true because we don't want other handlers for the same shortcut to be called later | ||
}}, | ||
{ shortcut: '-Ctrl+A' } // Removing the previous shortcut | ||
shortcuts.add ([ | ||
{ | ||
shortcut: 'Ctrl+A', | ||
handler: () => { | ||
// Doing something... | ||
return true; // Returning true if we don't want other handlers for the same shortcut to be called later | ||
} | ||
}, | ||
{ | ||
shortcut: 'Ctrl+B', | ||
handler: onCtrlB | ||
}, | ||
{ | ||
shortcut: 'CmdOrCtrl+K Shift+B', | ||
handler: () => { | ||
// Doing something... | ||
return true; // Returning true if we don't want other handlers for the same shortcut to be called later | ||
} | ||
}, | ||
{ | ||
shortcut: '-Ctrl+A' // Removing all previously-registered handlers for "Ctrl+A" | ||
} | ||
]); | ||
shortcuts.remove ({ shortcut: 'Ctrl-B', handler: CtrlBHandler }); // Removing a single handler | ||
shortcuts.remove ({ shortcut: 'Ctrl-A' }); // Removing all handlers bound to this shortcut | ||
shortcuts.remove ({ shortcut: 'Ctrl+B', handler: onCtrlB }); // Removing a specific handler bound to this shortcut | ||
shortcuts.remove ({ shortcut: 'Ctrl+A' }); // Removing all handlers bound to this shortcut | ||
shortcuts.reset (); // Removing all shortcuts | ||
// Let's actually start listening for shortcuts | ||
const dispose = shortcuts.record ( shortcut => { // Recording shortcuts | ||
console.log ( 'Shortcut recorded:', shortcut ); | ||
}); | ||
shortcuts.start (); | ||
dispose (); // Stopping recording | ||
``` | ||
// Let's stop listening for shortcuts | ||
- ℹ️ Handlers are called from the bottom to the top, so an handler defined at the bottom will take precedence over an handler for the same shortcut defined at the top. | ||
- ℹ️ If multiple handlers are defined for the same shortcut all of them are executed until one of them returns `true`. | ||
- ℹ️ Adding a shortcut starting with an hyphen (e.g. `-Ctrl-A`) will actually remove that shortcut. | ||
- ℹ️ While recording no handlers will be called. | ||
shortcuts.stop (); | ||
### Shortcut Object | ||
// Let's dispose of all registered shortcuts | ||
The Shortcut object provides some utilities that you might need in your application. This is its interface: | ||
```ts | ||
const Shortcut = { | ||
shortcut2id ( shortcut: string ): number[]; | ||
shortcut2accelerator ( shortcut: string ): string; | ||
shortcut2symbols ( shortcut: string ): string; | ||
}; | ||
shortcuts.reset (); | ||
``` | ||
Usage: | ||
```ts | ||
import {Shortcut} from 'shortcuts'; | ||
Shortcut.shortcut2accelerator ( 'Meta+Del' ); // => 'Cmd+Delete' | ||
Shortcut.shortcut2symbols ( 'Cmd+Shift+A' ); // => '⌘⇧A' | ||
``` | ||
## Thanks | ||
@@ -132,0 +109,0 @@ |
/* MAIN */ | ||
const MODIFIER_KEY_BITMASK = 0b11111111_00000000; // Bitmask that includes all modifier keys and none of the triggers | ||
const TRIGGER_KEY_BITMASK = 0b11111111; // Bitmask that includes all trigger keys and none of the modifiers | ||
const FORMAT = 'long-inflexible-directional'; | ||
const PLUSES_RE = /\+{2,}/gi; | ||
const WHITESPACE_RE = /\s+/gi; | ||
const SHORTCUT_RE = /^\s*?(?:(?:^-?|\s|\+)(?:alt|option|cmd|command|meta|ctrl|control|shift|cmdorctrl|commandorcontrol|backspace|capslock|del|delete|down|end|enter|return|esc|escape|home|insert|left|pagedown|pageup|right|space|spacebar|tab|up|plus|\d|[a-z]|f(?:\d|1\d|2[0-4])|numpad\d|[!"#$%&'()*+,./:;<=>?@[\]^_`{|}~-]))+\s*$/i; // Regex that matches a shortcut | ||
const RESULT = <const> { | ||
HANDLED: 0, // An handler caught that | ||
UNHANDLED: 1, // No handler caught that, but maybe a deeper one could | ||
UNHANDLEABLE: 2 // No handler caught that, and there are no deeper handlers that could catch that | ||
}; | ||
/* EXPORT */ | ||
export {MODIFIER_KEY_BITMASK, TRIGGER_KEY_BITMASK} | ||
export {PLUSES_RE, WHITESPACE_RE, SHORTCUT_RE}; | ||
export {RESULT}; | ||
export {FORMAT}; |
131
src/index.ts
/* IMPORT */ | ||
import Shortcut from './shortcut'; | ||
import Shortcuts from './shortcuts'; | ||
import ShoSho from 'shosho'; | ||
import {FORMAT} from './constants'; | ||
import {castArray} from './utils'; | ||
import type {Descriptor, Disposer, Handler, Options, Registration} from './types'; | ||
/* MAIN */ | ||
class Shortcuts { | ||
/* VARIABLES */ | ||
private registrations: Registration[] = []; | ||
private shosho: ShoSho; | ||
/* CONSTRUCTOR */ | ||
constructor ( options: Options ) { | ||
this.registrations = []; | ||
this.shosho = new ShoSho ( options ); | ||
} | ||
/* PUBLIC API */ | ||
get = (): Descriptor[] => { | ||
return this.registrations.map ( registration => registration.descriptor ); | ||
}; | ||
add = ( descriptor: Descriptor | Descriptor[] ): void => { | ||
const descriptors = castArray ( descriptor ); | ||
for ( const descriptor of descriptors ) { | ||
if ( descriptor.shortcut.startsWith ( '-' ) ) { | ||
this.remove ( descriptor ); | ||
} else if ( descriptor.handler ) { | ||
const dispose = this.shosho.register ( descriptor.shortcut, descriptor.handler ); | ||
const shortcut = ShoSho.format ( descriptor.shortcut, FORMAT ); | ||
const registration = { descriptor, dispose, shortcut }; | ||
this.registrations.push ( registration ); | ||
} else { | ||
console.error ( `Cannot register shortcut "${descriptor.shortcut}", which has no handler` ); | ||
} | ||
} | ||
}; | ||
register = ( descriptor: Descriptor | Descriptor[] ): Disposer => { | ||
this.add ( descriptor ); | ||
return (): void => { | ||
this.remove ( descriptor ); | ||
}; | ||
}; | ||
remove = ( descriptor: Descriptor | Descriptor[] ): void => { | ||
const descriptors = castArray ( descriptor ); | ||
for ( const descriptor of descriptors ) { | ||
const handler = descriptor.handler; | ||
const shortcut = ShoSho.format ( descriptor.shortcut.replace ( /^-/, '' ), FORMAT ); | ||
this.registrations = this.registrations.filter ( registration => { | ||
if ( ( registration.shortcut !== shortcut ) || ( handler && registration.descriptor.handler !== handler ) ) { | ||
return true; | ||
} else { | ||
registration.dispose (); | ||
return false; | ||
} | ||
}); | ||
} | ||
}; | ||
reset = (): void => { | ||
this.registrations = []; | ||
this.shosho.reset (); | ||
}; | ||
trigger = ( shortcut: string ): boolean => { | ||
return this.shosho.trigger ( shortcut ); | ||
}; | ||
start = (): void => { | ||
this.shosho.start (); | ||
}; | ||
stop = (): void => { | ||
this.shosho.stop (); | ||
}; | ||
} | ||
/* EXPORT */ | ||
export {Shortcut, Shortcuts}; | ||
export default Shortcuts; | ||
export type {Descriptor, Disposer, Handler, Options, Registration}; |
/* MAIN */ | ||
/* IMPORT */ | ||
type Chord = string; | ||
type ChordID = number; | ||
type Shortcut = string; | ||
type ShortcutID = ChordID[]; | ||
import type {Options} from 'shosho'; | ||
type Disposer = () => void; | ||
type RecordHandler = ( shortcut: Shortcut ) => void; | ||
type ShouldHandleEventFunction = ( event: KeyboardEvent ) => boolean; | ||
/* MAIN */ | ||
type ListenerOptions = { | ||
capture?: boolean, | ||
target?: Node, | ||
handler: ( id: ShortcutID, event: KeyboardEvent ) => 0 | 1 | 2 | ShortcutID, | ||
shouldHandleEvent?: ShouldHandleEventFunction | ||
type Descriptor = { | ||
handler?: Handler, | ||
shortcut: string | ||
}; | ||
type ShortcutsOptions = { | ||
capture?: boolean, | ||
target?: Node, | ||
shortcuts?: ShortcutDescriptor[], | ||
shouldHandleEvent?: ShouldHandleEventFunction | ||
type Disposer = { | ||
(): void | ||
}; | ||
type ShortcutsTree = { | ||
[id: number]: ShortcutsTree, | ||
parent?: ShortcutsTree, | ||
id?: ChordID, | ||
size: number, | ||
handlers: Function[] | ||
type Handler = { | ||
( event?: Event ): boolean | void | ||
}; | ||
type ShortcutDescriptor = { | ||
handler?: ( event: KeyboardEvent ) => boolean | void, | ||
type Registration = { | ||
descriptor: Descriptor, | ||
dispose: Disposer, | ||
shortcut: string | ||
@@ -42,4 +29,2 @@ }; | ||
export type {Chord, ChordID, Shortcut, ShortcutID}; | ||
export type {Disposer, RecordHandler, ShouldHandleEventFunction}; | ||
export type {ListenerOptions, ShortcutsOptions, ShortcutsTree, ShortcutDescriptor}; | ||
export type {Descriptor, Disposer, Handler, Options, Registration}; |
/* MAIN */ | ||
const Utils = { | ||
const castArray = <T> ( value: T | T[] ): T[] => { | ||
/* API */ | ||
return Array.isArray ( value ) ? value : [value]; | ||
isArray: ( value: unknown ): value is unknown[] => { | ||
return Array.isArray ( value ); | ||
}, | ||
isEqual: ( a: number[], b: number[] ): boolean => { | ||
if ( a.length !== b.length ) return false; | ||
for ( let i = 0, l = a.length; i < l; i++ ) { | ||
if ( a[i] !== b[i] ) return false; | ||
} | ||
return true; | ||
}, | ||
isFalsy: ( value: unknown ): boolean => { | ||
return !value; | ||
}, | ||
isKeyboardEvent: ( event: Event ): event is KeyboardEvent => { | ||
return event.type.startsWith ( 'key' ); | ||
}, | ||
isMac: (): boolean => { | ||
if ( typeof navigator !== 'object' ) return false; | ||
return /mac|ipod|iphone|ipad/i.test ( navigator.platform ); | ||
}, | ||
isTruthy: ( value: unknown ): boolean => { | ||
return !!value; | ||
}, | ||
memoize: <T, R> ( fn: (( arg: T ) => R) ): (( arg: T ) => R) => { | ||
const cache = new Map<T, R> (); | ||
return ( arg: T ): R => { | ||
const cached = cache.get ( arg ); | ||
if ( cached || cache.has ( arg ) ) return cached as R; //TSC | ||
const result = fn ( arg ); | ||
cache.set ( arg, result ); | ||
return result; | ||
}; | ||
} | ||
}; | ||
@@ -76,2 +12,2 @@ | ||
export default Utils; | ||
export {castArray}; |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
3
27711
1
19
552
113
1
+ Addedshosho@^1.4.0
+ Addedshosho@1.4.3(transitive)