@keybindy/core
Version:
A lightweight and framework-agnostic keyboard shortcut manager for web apps. Define, register, and handle keybindings with ease.
2 lines (1 loc) • 9.09 kB
JavaScript
var Keybindy=function(){"use strict";const e={ctrl:["ctrl (left)","ctrl (right)"],shift:["shift (left)","shift (right)"],alt:["alt (left)","alt (right)"],meta:["meta (left)","meta (right)","cmd"]};function t(t){const s=[[]];for(const o of t){const t=o.toLowerCase(),i=e[t]??[t],n=[];for(const e of s)for(const t of i)n.push([...e,t]);s.splice(0,s.length,...n)}return s}const s={ControlLeft:"Ctrl (Left)",ControlRight:"Ctrl (Right)",ShiftLeft:"Shift (Left)",ShiftRight:"Shift (Right)",AltLeft:"Alt (Left)",AltRight:"Alt (Right)",MetaLeft:"Meta (Left)",MetaRight:"Meta (Right)",OSLeft:"Meta (Left)",OSRight:"Meta (Right)",KeyA:"A",KeyB:"B",KeyC:"C",KeyD:"D",KeyE:"E",KeyF:"F",KeyG:"G",KeyH:"H",KeyI:"I",KeyJ:"J",KeyK:"K",KeyL:"L",KeyM:"M",KeyN:"N",KeyO:"O",KeyP:"P",KeyQ:"Q",KeyR:"R",KeyS:"S",KeyT:"T",KeyU:"U",KeyV:"V",KeyW:"W",KeyX:"X",KeyY:"Y",KeyZ:"Z",Digit0:"0",Digit1:"1",Digit2:"2",Digit3:"3",Digit4:"4",Digit5:"5",Digit6:"6",Digit7:"7",Digit8:"8",Digit9:"9",Numpad0:"Numpad 0",Numpad1:"Numpad 1",Numpad2:"Numpad 2",Numpad3:"Numpad 3",Numpad4:"Numpad 4",Numpad5:"Numpad 5",Numpad6:"Numpad 6",Numpad7:"Numpad 7",Numpad8:"Numpad 8",Numpad9:"Numpad 9",NumpadAdd:"Numpad +",NumpadSubtract:"Numpad -",NumpadMultiply:"Numpad *",NumpadDivide:"Numpad /",NumpadEnter:"Numpad Enter",NumpadDecimal:"Numpad .",NumpadEqual:"Numpad =",NumpadComma:"Numpad ,",NumpadParenLeft:"Numpad (",NumpadParenRight:"Numpad )",Minus:"-",Equal:"=",BracketLeft:"[",BracketRight:"]",Backslash:"\\",Semicolon:";",Quote:"'",Comma:",",Period:".",Slash:"/",Backquote:"`",IntlBackslash:"Intl \\",IntlRo:"Intl Ro",IntlYen:"Intl Yen",Escape:"Esc",Tab:"Tab",CapsLock:"Caps Lock",Enter:"Enter",Space:"Space",Backspace:"Backspace",NumLock:"Num Lock",ScrollLock:"Scroll Lock",Pause:"Pause",ContextMenu:"Context Menu",PrintScreen:"Print Screen",Insert:"Insert",Delete:"Delete",Home:"Home",End:"End",PageUp:"Page Up",PageDown:"Page Down",ArrowUp:"Arrow Up",ArrowDown:"Arrow Down",ArrowLeft:"Arrow Left",ArrowRight:"Arrow Right",F1:"F1",F2:"F2",F3:"F3",F4:"F4",F5:"F5",F6:"F6",F7:"F7",F8:"F8",F9:"F9",F10:"F10",F11:"F11",F12:"F12",F13:"F13",F14:"F14",F15:"F15",F16:"F16",F17:"F17",F18:"F18",F19:"F19",F20:"F20",F21:"F21",F22:"F22",F23:"F23",F24:"F24",AudioVolumeMute:"Volume Mute",AudioVolumeDown:"Volume Down",AudioVolumeUp:"Volume Up",VolumeMute:"Volume Mute",VolumeDown:"Volume Down",VolumeUp:"Volume Up",MediaTrackNext:"Media Next Track",MediaTrackPrevious:"Media Previous Track",MediaPlayPause:"Media Play/Pause",MediaStop:"Media Stop",MediaSelect:"Media Select",BrowserHome:"Browser Home",BrowserSearch:"Browser Search",BrowserFavorites:"Browser Favorites",BrowserRefresh:"Browser Refresh",BrowserStop:"Browser Stop",BrowserForward:"Browser Forward",BrowserBack:"Browser Back",LaunchApp1:"Launch App 1",LaunchApp2:"Launch App 2",LaunchMail:"Launch Mail",LaunchMediaPlayer:"Launch Media Player",LaunchCalculator:"Launch Calculator",Convert:"Convert",NonConvert:"Non Convert",KanaMode:"Kana Mode",Lang1:"Language 1",Lang2:"Language 2",Lang3:"Language 3",Lang4:"Language 4",Lang5:"Language 5",Power:"Power",Sleep:"Sleep",WakeUp:"Wake Up",Eject:"Eject",Undo:"Undo",Redo:"Redo",Copy:"Copy",Cut:"Cut",Paste:"Paste",Select:"Select",Again:"Again",Find:"Find",Open:"Open",Props:"Properties",Help:"Help",Fn:"Fn",BrightnessUp:"Brightness Up",BrightnessDown:"Brightness Down"};function o(e){return s[e].toLowerCase()||e.toLowerCase()}const i=(...e)=>{console.log("[Keybindy]",...e)},n=(...e)=>{console.warn("[Keybindy]",...e)};class a{scopeStack=["global"];pushScope(e){e&&(this.scopeStack.includes(e)||this.scopeStack.push(e))}popScope(){this.scopeStack.length>1&&this.scopeStack.pop()}swap(e,t){const s=this.scopeStack[t];this.scopeStack[t]=e,this.scopeStack[this.scopeStack.length-1]=s}setActiveScope(e){if(!e)return;if(!this.scopeStack.includes(e))return void n("Scope not found: "+e);const t=this.scopeStack.indexOf(e);-1!==t&&this.swap(this.getActiveScope(),t)}resetScope(){this.scopeStack=["global"]}getActiveScope(){return this.scopeStack[this.scopeStack.length-1]}isScopeActive(e){return this.getActiveScope()===(e||"global")}getScopes(){return[...this.scopeStack]}}class r{listeners=[];on(e){return this.listeners.push(e),()=>this.off(e)}off(e){this.listeners=this.listeners.filter((t=>t!==e))}emit(e){this.listeners.forEach((t=>t(e)))}}return class extends a{shortcuts=[];pressedKeys=new Set;typingEmitter=new r;activeSequences=[];onShortcutFired=()=>{};constructor(e){if("undefined"==typeof window)throw new Error("[Keybindy] Unsupported environment");super(),this.onShortcutFired=e||(()=>{}),this.start()}start(){window.addEventListener("keydown",this.handleKeyDown),window.addEventListener("keyup",this.handleKeyUp)}disableAll(e){e?this.shortcuts.forEach((t=>t.options?.scope===e?t.enabled=!1:null)):this.shortcuts.forEach((e=>e.enabled=!1))}enableAll(e){e?this.shortcuts.forEach((t=>t.options?.scope===e?t.enabled=!0:null)):this.shortcuts.forEach((e=>e.enabled=!0))}onTyping(e){return this.typingEmitter.on(e)}handleKeyDown=e=>{const t=o(e.code).toLowerCase(),s=Date.now();this.pressedKeys.add(t),this.typingEmitter.emit({key:e.key,event:e});const i=new Set;for(const o of this.shortcuts){const{options:n,enabled:a,keys:r,handler:c}=o;if(!a)continue;if(n?.scope&&n.scope!==this.getActiveScope())continue;const u=r.map((e=>e.toLowerCase()));if(n?.sequential){const a=n.sequenceDelay??1e3;let r=this.activeSequences.find((e=>JSON.stringify(e.keys)===JSON.stringify(u)));if(r){r.buffer.push({key:t,time:s}),r.buffer=r.buffer.filter((e=>s-e.time<=a));const p=r.buffer.map((e=>e.key)),h=u.every(((e,t)=>p[t]===e));if(u.length===p.length&&!h){this.clearSequence(r.keys);continue}if(r.buffer.length>1){let e=1/0;for(let t=1;t<r.buffer.length;t++){const s=r.buffer[t].time-r.buffer[t-1].time;s<e&&(e=s)}if(e<100){this.clearSequence(r.keys);continue}}if(p.length===u.length&&p.every((e=>this.pressedKeys.has(e)))){this.clearSequence(r.keys);continue}h&&u.length===p.length&&(n.preventDefault&&e.preventDefault(),c(e),this.onShortcutFired(o),i.add(o.id),this.clearSequence(r.keys))}else t===u[0]&&(r={keys:u,buffer:[{key:t,time:s}]},this.activeSequences.push(r))}else{const t=u.every((e=>this.pressedKeys.has(e))),s=this.activeSequences.find((e=>JSON.stringify(e.keys)===JSON.stringify(u)));if(t&&!s)return n?.preventDefault&&e.preventDefault(),c(e),void this.onShortcutFired(o)}}this.activeSequences=this.activeSequences.filter((e=>{const t=this.shortcuts.find((t=>JSON.stringify(t.keys)===JSON.stringify(e.keys)))?.options?.sequenceDelay??1e3;return s-e.buffer[0]?.time<=t}))};clearSequence(e){this.activeSequences=this.activeSequences.filter((t=>JSON.stringify(t.keys)!==JSON.stringify(e)))}handleKeyUp=e=>{const t=o(e.code).toLowerCase();this.pressedKeys.delete(t)};register(e,s,o){const i=Array.isArray(e[0])?e:[e],n=o?.data?.id||"uid-"+Date.now().toString(36)+"-"+Math.random().toString(36).substring(2,8);for(const e of i){const i=t(e);for(const e of i){const t=e.map((e=>e.toLowerCase()));this.shortcuts=this.shortcuts.filter((e=>JSON.stringify(e.keys)!==JSON.stringify(t)||e.options?.scope!==(o?.scope||this.getActiveScope())||e.id!==n)),this.shortcuts.push({id:n,keys:t,handler:s,options:{...o,sequential:o?.sequential||!1,sequenceDelay:o?.sequenceDelay||1e3,scope:o?.scope||this.getActiveScope()},enabled:!0}),this.pushScope(o?.scope??"global")}}}unregister(e,s="global"){const o=t(e);for(const e of o){const t=e.map((e=>e.toLowerCase()));this.shortcuts=this.shortcuts.filter((e=>e.options?.scope!==s||JSON.stringify(e.keys)!==JSON.stringify(t)))}}toggleState(e,s,o){const i=t(e);let a=!1;for(const e of i){const t=e.map((e=>e.toLowerCase()));this.shortcuts.forEach((e=>{const i=!e.options?.scope||e.options.scope===s;JSON.stringify(e.keys)===JSON.stringify(t)&&i&&(a=!0,e.enabled="toggle"===o?!e.enabled:o)}))}a||n(`No matching shortcut for ${JSON.stringify(e)} in scope "${s}"`)}enable(e,t="global"){this.toggleState(e,t,!0)}disable(e,t="global"){this.toggleState(e,t,!1)}toggle(e,t="global"){this.toggleState(e,t,"toggle")}clear(){this.pressedKeys.clear(),window.removeEventListener("keydown",this.handleKeyDown),window.removeEventListener("keyup",this.handleKeyUp),i("Instance cleared")}destroy(){this.clear(),this.shortcuts=[],this.resetScope(),this.activeSequences=[],i("Instance destroyed")}getCheatSheet(e=this.getActiveScope()){const t=new Map;for(const s of this.shortcuts){if(s.options?.scope&&s.options.scope!==e)continue;const o=s.id,i=s.keys.map((e=>e.startsWith("ctrl")?"ctrl":e.startsWith("shift")?"shift":e.startsWith("alt")?"alt":e.startsWith("meta")?"meta":e)).join(s.options?.sequential?" → ":" + ").toUpperCase();t.has(o)?t.get(o).keys.add(i):t.set(o,{keys:new Set([i]),data:s.options?.data??{}})}return Array.from(t.values()).map((e=>({keys:Array.from(e.keys),...e.data})))}getScopesInfo(e){const t={};for(const s of this.shortcuts){const o=s.options?.scope||"global";e&&o!==e||(t[o]||(t[o]={shortcuts:[]}),t[o].shortcuts.push({keys:s.keys.map((e=>e.toUpperCase())),id:s.id,enabled:s.enabled??!0,data:s.options?.data??{}}),o===this.getActiveScope()&&(t[o].isActive=!0))}return e?t[e]||null:t}}}();