hotkey-router 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/dist/auto.js +6 -4
- package/dist/auto.js.map +2 -2
- package/dist/auto.min.js +2 -2
- package/dist/hotkey-router.cjs +6 -4
- package/dist/hotkey-router.cjs.map +2 -2
- package/dist/hotkey-router.js +6 -4
- package/dist/hotkey-router.js.map +2 -2
- package/dist/hotkey-router.min.js +2 -2
- package/dist/reservations.js +1 -1
- package/dist/reservations.js.map +2 -2
- package/package.json +11 -5
package/dist/auto.min.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
// hotkey-router v0.2.
|
|
1
|
+
// hotkey-router v0.2.3 | SEE LICENSE IN LICENSE
|
|
2
2
|
|
|
3
|
-
function R(t,e,r,a){return t.addEventListener(e,r,a),()=>t.removeEventListener(e,r,a)}var W=typeof navigator<"u"&&/Mac|iPhone|iPad|iPod/.test(navigator.platform),H=new Set(["ctrl","shift","alt","meta"]),U=Object.freeze({Alt:"alt",AltGraph:"alt",Control:"ctrl",Shift:"shift",Meta:"meta",OS:"meta"}),V=new Map([["^","ctrl"],["!","alt"],["+","shift"],["#","meta"]]),A=Object.freeze({esc:"escape",escape:"escape",space:" ",spacebar:" ",enter:"enter",return:"enter",del:"delete",delete:"delete",backspace:"backspace",tab:"tab",up:"arrowup",down:"arrowdown",left:"arrowleft",right:"arrowright",home:"home",end:"end",pageup:"pageup",pagedown:"pagedown",pgup:"pageup",pgdn:"pagedown",cmd:"meta",command:"meta",win:"meta",meta:"meta",control:"ctrl",ctrl:"ctrl",option:"alt",alt:"alt",shift:"shift","\u2318":"meta","\u2303":"ctrl","\u2325":"alt","\u21E7":"shift",mod:W?"meta":"ctrl",plus:"+"}),M=!0,C=typeof window<"u"?window:null,p=[],u=new Map,w=new Map,v=new Map,E=!1,q=1;function F(t){let e=String(t??"").trim(),r=e.toLowerCase();return A[r]??(e===" "?" ":r)}function S({ctrl:t,shift:e,alt:r,meta:a,key:i,code:o,bareModifier:n}){if(n)return`__bare__:${n}`;let s="";return t&&(s+="ctrl+"),e&&(s+="shift+"),r&&(s+="alt+"),a&&(s+="meta+"),o?s+"code:"+o:s?s+i:i}function G(t){let e=U[t.key];return e?`__bare__:${e}`:S({ctrl:t.ctrlKey,shift:t.shiftKey,alt:t.altKey,meta:t.metaKey,key:F(t.key)})}function X(t){if(!t.code)return null;let e="";return t.ctrlKey&&(e+="ctrl+"),t.shiftKey&&(e+="shift+"),t.altKey&&(e+="alt+"),t.metaKey&&(e+="meta+"),e+"code:"+t.code}function Z(t){let e=F(t);return H.has(e)}var Q=/(^|\+)code:([A-Za-z][A-Za-z0-9_]*)/;function P(t){let e=String(t||"").trim(),r=/\s+up$/i.test(e),a=r?e.replace(/\s+up$/i,"").trim():e,i={ctrl:!1,shift:!1,alt:!1,meta:!1,key:null,code:null,bareModifier:null},o=a;for(;o.length;){let c=V.get(o[0])??null;if(!c)break;i[c]=!0,o=o.slice(1).trimStart()}let n=o.match(/code:[A-Za-z][A-Za-z0-9_]*/g);if(n&&n.length>1)throw new Error(`Invalid hotkey "${t}": multiple code: tokens not supported (got ${n.length}).`);let s=null,d=o.replace(Q,(c,y,j)=>(s=j,y+"__code__")).toLowerCase(),h=d.split("+").map(c=>c.trim()),m=h.filter(Boolean),f=h.some(c=>c==="")&&d.includes("++");if(!f){let c=!1;for(let y of m)if(Z(y)){if(c)throw new Error(`Invalid hotkey "${t}": modifiers must come before the base key (e.g. "ctrl+k", not "k+ctrl")`)}else c||(c=!0)}for(let c of m){if(c==="__code__"){i.code=s;continue}let y=A[c]??c;y==="ctrl"?i.ctrl=!0:y==="shift"?i.shift=!0:y==="alt"?i.alt=!0:y==="meta"?i.meta=!0:i.key=F(y)}if(!i.key&&!i.code&&f&&(i.key="+"),!i.key&&!i.code){let c=(i.ctrl?1:0)+(i.shift?1:0)+(i.alt?1:0)+(i.meta?1:0);if(c===1)i.bareModifier=i.ctrl?"ctrl":i.shift?"shift":i.alt?"alt":"meta";else throw c>1?new Error(`Invalid hotkey "${t}": multi-modifier bare bindings not supported. Add a base key (e.g. "ctrl+alt+x").`):new Error(`Invalid hotkey "${t}" (missing base key)`)}return{combo:i,type:r?"keyup":"keydown",raw:t}}function Y(t){let e=t?.target;if(!e)return!1;let r=String(e.tagName||"").toUpperCase();return e.isContentEditable||r==="INPUT"||r==="TEXTAREA"||r==="SELECT"||e.getAttribute?.("role")==="textbox"}function J(t,e){let r=e?{...e}:{};return t==="keydown"&&r.repeat==null&&(r.repeat=!1),r.priority==null&&(r.priority=0),r}function tt(t,e){let r=u.get(t);r||(r=new Map,u.set(t,r));let a=r.get(e);return a||(a=[],r.set(e,a)),a}function x(t){let e=v.get(t);if(!e)return!1;let{comboStr:r,type:a}=e,i=u.get(r),o=i?.get(a);if(!o?.length)return v.delete(t),!1;let n=o.filter(s=>s.id!==t);return n.length?i.set(a,n):i.delete(a),i.size||u.delete(r),v.delete(t),!0}function D(t,e,r=null,a={}){let{combo:i,type:o,raw:n}=P(t),s=S(i),l=J(o,a),d={id:q++,comboStr:s,type:o,combo:i,handler:e,plugin:r,raw:n,options:l};if(tt(s,o).push(d),v.set(d.id,{comboStr:s,type:o}),p.length)for(let m of p)try{m({combo:i,raw:n,options:l,plugin:r,id:d.id})}catch(f){console.error("[hotkey-router] onBind hook threw:",f)}let h=()=>x(d.id);return h.id=d.id,h.hotkey=n,h}function et(t){if(typeof t!="function")throw new TypeError("hotkeys.onBind(hook) expects a function");return p.push(t),()=>{p=p.filter(e=>e!==t)}}function rt(t,e){let{combo:r,type:a}=P(t),i=S(r),o=u.get(i);if(!o)return;let n=o.get(a);if(n?.length){if(e){let s=[];for(let l of n)l.handler===e?v.delete(l.id):s.push(l);s.length?o.set(a,s):o.delete(a)}else{for(let s of n)v.delete(s.id);o.delete(a)}o.size||u.delete(i)}}function it(t,e){if(w.has(t))throw new Error(`Plugin "${t}" already registered`);let r=[];for(let[a,i]of Object.entries(e)){let[o,n]=Array.isArray(i)?i:[i,{}],s=D(a,o,t,n);r.push(s.id)}return w.set(t,r),()=>I(t)}function I(t){let e=w.get(t);if(e){for(let r of e)x(r);w.delete(t)}}function at(){E=!0}function ot(){E=!1}function st(t=!0){M=!!t}function nt(t){C=t}function B(t){let e=null;for(let r of t)e?(r.options.priority>e.options.priority||r.options.priority===e.options.priority&&r.id>e.id)&&(e=r):e=r;return e}function T(t){return e=>{if(E)return;let r=M&&Y(e),a=G(e),i=X(e),o=new Set,n=[];for(let d of[a,i]){if(!d||o.has(d))continue;o.add(d);let h=u.get(d)?.get(t);if(h?.length)for(let m of h){let f=m.options;r&&!f.allowIn?.(e)||t==="keydown"&&f.repeat===!1&&e.repeat||f.when&&f.when(e)===!1||n.push(m)}}if(!n.length)return;let s=B(n),l=s.options;l.preventDefault&&e.preventDefault(),l.stopImmediatePropagation?e.stopImmediatePropagation():l.stopPropagation&&e.stopPropagation(),s.handler(e),l.once&&x(s.id)}}var b=null,k=null;function _({target:t=C,capture:e=!1}={}){if(!t)throw new Error("hotkeys.init() requires a target (e.g. window)");(b||k)&&z(),b=R(t,"keydown",T("keydown"),{capture:e}),k=R(t,"keyup",T("keyup"),{capture:e})}function z(){b?.(),k?.(),b=null,k=null,u.clear(),w.clear(),v.clear()}var ct={ctrl:"Control",shift:"Shift",alt:"Alt",meta:"Meta"};function lt(t,{type:e}={}){let{combo:r,type:a}=P(t),i=S(r),o=e||a,n=u.get(i)?.get(o);if(!n?.length)return!1;let s=r.bareModifier?ct[r.bareModifier]:r.key,l=!!r.alt,d=!!r.ctrl,h=!!r.shift,m=!!r.meta;r.bareModifier&&o==="keyup"&&(r.bareModifier==="alt"?l=!1:r.bareModifier==="ctrl"?d=!1:r.bareModifier==="shift"?h=!1:r.bareModifier==="meta"&&(m=!1));let f={type:o,key:s,code:r.code||null,ctrlKey:d,shiftKey:h,altKey:l,metaKey:m,repeat:!1,preventDefault(){},stopPropagation(){},stopImmediatePropagation(){},target:null},c=B(n.filter(y=>y.options.when?y.options.when(f)!==!1:!0));return c?(c.handler(f),c.options.once&&x(c.id),!0):!1}C&&_();var O={init:_,destroy:z,setTarget:nt,bind:D,unbind:rt,registerPlugin:it,unregisterPlugin:I,onBind:et,pause:at,resume:ot,ignoreInput:st,trigger:lt};var g={$comment:"Machine-readable companion to docs/browser-hotkeys.md. Each combo entry describes who claims the binding and how strongly. Intended for runtime conflict warnings in keyboard-routing libraries (e.g. hotkey-router) and dev-time validation in apps like WebRadar. Update both this file and the markdown together \u2014 the markdown is the human source of truth, this file is the machine projection.",version:{generated:"2026-05-17",schema:"1.0",sources:["https://support.mozilla.org/en-US/kb/keyboard-shortcuts-perform-firefox-tasks-quickly","https://firefox-source-docs.mozilla.org/devtools-user/keyboard_shortcuts/index.html","https://support.google.com/chrome/answer/157179","https://developer.chrome.com/docs/devtools/shortcuts","https://support.apple.com/guide/safari/keyboard-shortcuts-and-gestures-cpsh003/mac","https://developer.apple.com/documentation/safari-developer-tools/web-inspector","https://support.microsoft.com/en-us/microsoft-edge/keyboard-shortcuts-in-microsoft-edge-50d3edab-30d9-c7e4-21ce-37fe2713cfad","https://support.apple.com/en-us/102650","https://support.microsoft.com/en-us/windows/keyboard-shortcuts-in-windows-dcc61a57-8ff0-cffe-9796-cb9706c75eec","https://help.gnome.org/gnome-help/shell-keyboard-shortcuts.html"]},severityLevels:{hard:"Browser or OS intercepts before page-world keydown fires. preventDefault() can't recover it.","find-bar-only":"Reserved only when the browser's find bar has focus. Fires in page world during normal browsing.","menu-activation":"Win/Linux Alt+letter activates the browser menu bar. Hard to suppress reliably.","system-text":"Only reserved while a text input has focus (terminal-style cursor controls on macOS).",compose:"macOS Option+letter types a special character in text inputs. Outside inputs, free for page world.",os:"Operating system intercepts globally, before any application sees it.","devtools-open":"Only relevant when DevTools is already open; outside DevTools it fires normally."},$keyFormat:"Each combo key uses hotkey-router-compatible syntax: modifiers in canonical order (ctrl|meta|alt|shift) joined by '+', then the key (lowercased letter, or symbolic name). Per-platform tables use the platform's native modifier name \u2014 `meta` on macOS = Cmd, `meta` on Windows = Win, `ctrl` on Linux/Windows = the primary modifier.",platforms:{mac:{browsers:{firefox:{"meta+a":{action:"Select all",severity:"hard"},"meta+b":{action:"Bookmarks sidebar",severity:"hard"},"meta+c":{action:"Copy",severity:"hard"},"meta+d":{action:"Bookmark this page",severity:"hard"},"meta+e":{action:"Search from page",severity:"hard"},"meta+f":{action:"Find in page",severity:"hard"},"meta+g":{action:"Find next",severity:"hard"},"meta+j":{action:"Downloads",severity:"hard"},"meta+l":{action:"Focus address bar",severity:"hard"},"meta+n":{action:"New window",severity:"hard"},"meta+o":{action:"Open file",severity:"hard"},"meta+p":{action:"Print",severity:"hard"},"meta+r":{action:"Reload",severity:"hard"},"meta+s":{action:"Save page",severity:"hard"},"meta+t":{action:"New tab",severity:"hard"},"meta+u":{action:"View source",severity:"hard"},"meta+v":{action:"Paste",severity:"hard"},"meta+w":{action:"Close tab",severity:"hard"},"meta+x":{action:"Cut",severity:"hard"},"meta+y":{action:"Redo",severity:"hard"},"meta+z":{action:"Undo",severity:"hard"},"meta+shift+a":{action:"Add-ons manager",severity:"hard"},"meta+shift+b":{action:"Bookmarks toolbar",severity:"hard"},"meta+shift+c":{action:"Inspector (element picker)",severity:"hard"},"meta+shift+d":{action:"Bookmark all tabs",severity:"hard"},"meta+shift+e":{action:"Network Monitor",severity:"hard"},"meta+shift+f":{action:"Toggle fullscreen",severity:"hard"},"meta+shift+g":{action:"Find previous",severity:"find-bar-only"},"meta+shift+j":{action:"Browser Console",severity:"hard"},"meta+shift+n":{action:"New private window",severity:"hard"},"meta+shift+o":{action:"Bookmarks library",severity:"hard"},"meta+shift+p":{action:"New private window (alt)",severity:"hard"},"meta+shift+q":{action:"Quit (with confirm)",severity:"hard"},"meta+shift+s":{action:"Firefox Screenshot",severity:"hard"},"meta+shift+t":{action:"Reopen closed tab",severity:"hard"},"meta+shift+v":{action:"Paste as plain text",severity:"hard"},"meta+shift+w":{action:"Close window",severity:"hard"},"meta+shift+z":{action:"Redo",severity:"hard"},"meta+alt+c":{action:"Inspector",severity:"hard"},"meta+alt+e":{action:"Network Monitor",severity:"hard"},"meta+alt+i":{action:"DevTools Toolbox",severity:"hard"},"meta+alt+j":{action:"Browser Console",severity:"hard"},"meta+alt+k":{action:"Web Console",severity:"hard"},"meta+alt+m":{action:"Responsive Design Mode",severity:"hard"},"meta+alt+z":{action:"Debugger",severity:"hard"}},chrome:{"meta+a":{action:"Select all",severity:"hard"},"meta+c":{action:"Copy",severity:"hard"},"meta+d":{action:"Bookmark this page",severity:"hard"},"meta+e":{action:"Search selected text",severity:"find-bar-only"},"meta+f":{action:"Find in page",severity:"hard"},"meta+g":{action:"Find next",severity:"hard"},"meta+l":{action:"Focus address bar",severity:"hard"},"meta+n":{action:"New window",severity:"hard"},"meta+o":{action:"Open file",severity:"hard"},"meta+p":{action:"Print",severity:"hard"},"meta+r":{action:"Reload",severity:"hard"},"meta+s":{action:"Save page",severity:"hard"},"meta+t":{action:"New tab",severity:"hard"},"meta+v":{action:"Paste",severity:"hard"},"meta+w":{action:"Close tab",severity:"hard"},"meta+x":{action:"Cut",severity:"hard"},"meta+y":{action:"History page",severity:"hard"},"meta+z":{action:"Undo",severity:"hard"},"meta+shift+b":{action:"Bookmarks bar toggle",severity:"hard"},"meta+shift+c":{action:"DevTools Inspector",severity:"hard"},"meta+shift+d":{action:"Bookmark all tabs",severity:"hard"},"meta+shift+g":{action:"Find previous",severity:"find-bar-only"},"meta+shift+j":{action:"Downloads page",severity:"hard"},"meta+shift+m":{action:"Switch user / guest",severity:"hard"},"meta+shift+n":{action:"New incognito window",severity:"hard"},"meta+shift+r":{action:"Hard reload",severity:"hard"},"meta+shift+t":{action:"Reopen closed tab",severity:"hard"},"meta+shift+v":{action:"Paste as plain text",severity:"hard"},"meta+shift+w":{action:"Close window",severity:"hard"},"meta+shift+z":{action:"Redo",severity:"hard"},"meta+alt+i":{action:"DevTools",severity:"hard"},"meta+alt+j":{action:"DevTools (console)",severity:"hard"},"meta+alt+u":{action:"View source",severity:"hard"}},safari:{"meta+a":{action:"Select all",severity:"hard"},"meta+c":{action:"Copy",severity:"hard"},"meta+d":{action:"Add to Reading List",severity:"hard"},"meta+f":{action:"Find in page",severity:"hard"},"meta+g":{action:"Find next",severity:"hard"},"meta+j":{action:"Downloads",severity:"hard"},"meta+l":{action:"Focus Smart Search",severity:"hard"},"meta+n":{action:"New window",severity:"hard"},"meta+p":{action:"Print",severity:"hard"},"meta+r":{action:"Reload",severity:"hard"},"meta+s":{action:"Save page",severity:"hard"},"meta+t":{action:"New tab",severity:"hard"},"meta+v":{action:"Paste",severity:"hard"},"meta+w":{action:"Close tab",severity:"hard"},"meta+x":{action:"Cut",severity:"hard"},"meta+z":{action:"Undo",severity:"hard"},"meta+shift+c":{action:"Inspector pick element",severity:"hard"},"meta+shift+d":{action:"Add Reading List item",severity:"hard"},"meta+shift+g":{action:"Find previous",severity:"find-bar-only"},"meta+shift+h":{action:"Go to homepage",severity:"hard"},"meta+shift+n":{action:"New private window",severity:"hard"},"meta+shift+r":{action:"Reader mode",severity:"hard"},"meta+shift+t":{action:"Reopen last closed tab",severity:"hard"},"meta+shift+v":{action:"Paste as plain text",severity:"hard"},"meta+shift+w":{action:"Close window",severity:"hard"},"meta+shift+z":{action:"Redo",severity:"hard"},"meta+alt+c":{action:"Web Inspector pick",severity:"hard"},"meta+alt+i":{action:"Web Inspector",severity:"hard"}}},system:{"meta+space":{action:"Spotlight",severity:"os"},"meta+tab":{action:"App switcher",severity:"os"},"meta+h":{action:"Hide app",severity:"os"},"meta+m":{action:"Minimize window",severity:"os"},"meta+q":{action:"Quit app",severity:"os"},"meta+shift+3":{action:"Screenshot full screen",severity:"os"},"meta+shift+4":{action:"Screenshot selection",severity:"os"},"meta+shift+5":{action:"Screen recording tool",severity:"os"},"meta+shift+q":{action:"Log out",severity:"os"},"meta+ctrl+q":{action:"Lock screen",severity:"os"},"meta+ctrl+space":{action:"Character Viewer",severity:"os"},"meta+alt+esc":{action:"Force-quit dialog",severity:"os"}},noteForLetterCombos:{"alt+a..z":{behavior:"compose",note:"Option+letter types special characters in text inputs (Option+E\u2192\xB4, Option+A\u2192\xE5, Option+F\u2192\u0192). Outside inputs, free for page world."},"ctrl+a..z":{behavior:"system-text",note:"Mac uses Ctrl+letter as terminal-style cursor controls inside text inputs (Ctrl+A=line start, Ctrl+E=line end, Ctrl+K=delete-to-end, etc.). Outside inputs, free for page world."}}},windows:{browsers:{firefox:{"ctrl+a":{action:"Select all",severity:"hard"},"ctrl+b":{action:"Bookmarks sidebar",severity:"hard"},"ctrl+c":{action:"Copy",severity:"hard"},"ctrl+d":{action:"Bookmark this page",severity:"hard"},"ctrl+f":{action:"Find in page",severity:"hard"},"ctrl+g":{action:"Find next",severity:"hard"},"ctrl+h":{action:"History sidebar",severity:"hard"},"ctrl+j":{action:"Downloads",severity:"hard"},"ctrl+l":{action:"Focus address bar",severity:"hard"},"ctrl+n":{action:"New window",severity:"hard"},"ctrl+o":{action:"Open file",severity:"hard"},"ctrl+p":{action:"Print",severity:"hard"},"ctrl+r":{action:"Reload",severity:"hard"},"ctrl+s":{action:"Save page",severity:"hard"},"ctrl+t":{action:"New tab",severity:"hard"},"ctrl+u":{action:"View source",severity:"hard"},"ctrl+v":{action:"Paste",severity:"hard"},"ctrl+w":{action:"Close tab",severity:"hard"},"ctrl+x":{action:"Cut",severity:"hard"},"ctrl+y":{action:"Redo",severity:"hard"},"ctrl+z":{action:"Undo",severity:"hard"},"ctrl+shift+a":{action:"Add-ons manager",severity:"hard"},"ctrl+shift+b":{action:"Bookmarks toolbar",severity:"hard"},"ctrl+shift+c":{action:"Inspector (element picker)",severity:"hard"},"ctrl+shift+d":{action:"Bookmark all tabs",severity:"hard"},"ctrl+shift+e":{action:"Network Monitor",severity:"hard"},"ctrl+shift+g":{action:"Find previous",severity:"find-bar-only"},"ctrl+shift+i":{action:"Developer Tools",severity:"hard"},"ctrl+shift+j":{action:"Browser Console",severity:"hard"},"ctrl+shift+k":{action:"Web Console",severity:"hard"},"ctrl+shift+m":{action:"Responsive Design Mode",severity:"hard"},"ctrl+shift+n":{action:"New private window",severity:"hard"},"ctrl+shift+o":{action:"Bookmarks library",severity:"hard"},"ctrl+shift+p":{action:"New private window (alt)",severity:"hard"},"ctrl+shift+s":{action:"Firefox Screenshot",severity:"hard"},"ctrl+shift+t":{action:"Reopen closed tab",severity:"hard"},"ctrl+shift+v":{action:"Paste as plain text",severity:"hard"},"ctrl+shift+w":{action:"Close window",severity:"hard"},"ctrl+shift+y":{action:"Downloads (alt)",severity:"hard"},"ctrl+shift+z":{action:"Debugger",severity:"hard"},"alt+f":{action:"File menu",severity:"menu-activation"},"alt+e":{action:"Edit menu",severity:"menu-activation"},"alt+v":{action:"View menu",severity:"menu-activation"},"alt+b":{action:"Bookmarks menu",severity:"menu-activation"},"alt+t":{action:"Tools menu",severity:"menu-activation"},"alt+h":{action:"Help menu",severity:"menu-activation"},"alt+d":{action:"Focus address bar",severity:"hard"}},chrome:{"ctrl+a":{action:"Select all",severity:"hard"},"ctrl+c":{action:"Copy",severity:"hard"},"ctrl+d":{action:"Bookmark this page",severity:"hard"},"ctrl+e":{action:"Search from page",severity:"hard"},"ctrl+f":{action:"Find in page",severity:"hard"},"ctrl+g":{action:"Find next",severity:"hard"},"ctrl+h":{action:"History page",severity:"hard"},"ctrl+j":{action:"Downloads",severity:"hard"},"ctrl+k":{action:"Search from page",severity:"hard"},"ctrl+l":{action:"Focus address bar",severity:"hard"},"ctrl+n":{action:"New window",severity:"hard"},"ctrl+o":{action:"Open file",severity:"hard"},"ctrl+p":{action:"Print",severity:"hard"},"ctrl+r":{action:"Reload",severity:"hard"},"ctrl+s":{action:"Save page",severity:"hard"},"ctrl+t":{action:"New tab",severity:"hard"},"ctrl+u":{action:"View source",severity:"hard"},"ctrl+v":{action:"Paste",severity:"hard"},"ctrl+w":{action:"Close tab",severity:"hard"},"ctrl+x":{action:"Cut",severity:"hard"},"ctrl+z":{action:"Undo",severity:"hard"},"ctrl+shift+b":{action:"Bookmarks bar toggle",severity:"hard"},"ctrl+shift+d":{action:"Bookmark all tabs",severity:"hard"},"ctrl+shift+g":{action:"Find previous",severity:"find-bar-only"},"ctrl+shift+i":{action:"DevTools",severity:"hard"},"ctrl+shift+j":{action:"DevTools (console)",severity:"hard"},"ctrl+shift+m":{action:"Switch user / guest",severity:"hard"},"ctrl+shift+n":{action:"Incognito window",severity:"hard"},"ctrl+shift+r":{action:"Hard reload",severity:"hard"},"ctrl+shift+t":{action:"Reopen closed tab",severity:"hard"},"ctrl+shift+v":{action:"Paste as plain text",severity:"hard"},"ctrl+shift+w":{action:"Close window",severity:"hard"},"alt+d":{action:"Focus address bar",severity:"hard"},"alt+e":{action:"Customize menu",severity:"menu-activation"},"alt+f":{action:"Customize menu (alt)",severity:"menu-activation"}},edge:{"ctrl+a":{action:"Select all",severity:"hard"},"ctrl+c":{action:"Copy",severity:"hard"},"ctrl+d":{action:"Favorite this page",severity:"hard"},"ctrl+e":{action:"Search from page",severity:"hard"},"ctrl+f":{action:"Find in page",severity:"hard"},"ctrl+g":{action:"Find next",severity:"hard"},"ctrl+h":{action:"History",severity:"hard"},"ctrl+j":{action:"Downloads",severity:"hard"},"ctrl+k":{action:"Search from page",severity:"hard"},"ctrl+l":{action:"Focus address bar",severity:"hard"},"ctrl+m":{action:"Mute current tab",severity:"hard"},"ctrl+n":{action:"New window",severity:"hard"},"ctrl+o":{action:"Open file",severity:"hard"},"ctrl+p":{action:"Print",severity:"hard"},"ctrl+r":{action:"Reload",severity:"hard"},"ctrl+s":{action:"Save page",severity:"hard"},"ctrl+t":{action:"New tab",severity:"hard"},"ctrl+u":{action:"View source",severity:"hard"},"ctrl+v":{action:"Paste",severity:"hard"},"ctrl+w":{action:"Close tab",severity:"hard"},"ctrl+x":{action:"Cut",severity:"hard"},"ctrl+z":{action:"Undo",severity:"hard"},"ctrl+shift+b":{action:"Favorites bar toggle",severity:"hard"},"ctrl+shift+d":{action:"Bookmark all tabs in folder",severity:"hard"},"ctrl+shift+e":{action:"Search in sidebar",severity:"hard"},"ctrl+shift+g":{action:"Find previous",severity:"find-bar-only"},"ctrl+shift+i":{action:"DevTools",severity:"hard"},"ctrl+shift+j":{action:"DevTools (console)",severity:"hard"},"ctrl+shift+k":{action:"Duplicate tab",severity:"hard"},"ctrl+shift+l":{action:"Paste and search",severity:"hard"},"ctrl+shift+m":{action:"Switch user / guest",severity:"hard"},"ctrl+shift+n":{action:"InPrivate window",severity:"hard"},"ctrl+shift+o":{action:"Open favorites",severity:"hard"},"ctrl+shift+p":{action:"Print (system dialog)",severity:"hard"},"ctrl+shift+r":{action:"Hard reload",severity:"hard"},"ctrl+shift+t":{action:"Reopen closed tab",severity:"hard"},"ctrl+shift+u":{action:"Read Aloud toggle",severity:"hard"},"ctrl+shift+v":{action:"Paste without formatting",severity:"hard"},"ctrl+shift+w":{action:"Close window",severity:"hard"},"ctrl+shift+y":{action:"Collections",severity:"hard"},"alt+d":{action:"Focus address bar",severity:"hard"},"alt+e":{action:"Settings menu",severity:"menu-activation"},"alt+f":{action:"Settings menu (alt)",severity:"menu-activation"},"alt+shift+b":{action:"Focus favorites bar",severity:"hard"},"alt+shift+i":{action:"Send feedback dialog",severity:"hard"},"alt+shift+t":{action:"Focus app bar toolbar",severity:"hard"}}},system:{"meta+a":{action:"Action Center",severity:"os"},"meta+c":{action:"Windows search",severity:"os"},"meta+d":{action:"Show desktop",severity:"os"},"meta+e":{action:"File Explorer",severity:"os"},"meta+g":{action:"Game bar",severity:"os"},"meta+h":{action:"Voice dictation",severity:"os"},"meta+i":{action:"Settings",severity:"os"},"meta+l":{action:"Lock",severity:"os"},"meta+n":{action:"Notification center",severity:"os"},"meta+p":{action:"Project / display mode",severity:"os"},"meta+r":{action:"Run dialog",severity:"os"},"meta+s":{action:"Search",severity:"os"},"meta+v":{action:"Clipboard history",severity:"os"},"meta+w":{action:"Widgets",severity:"os"},"meta+x":{action:"Quick Link menu",severity:"os"},"meta+z":{action:"Snap layouts",severity:"os"},"meta+tab":{action:"Task View",severity:"os"},"meta+shift+s":{action:"Screenshot region",severity:"os"},"ctrl+shift+esc":{action:"Task Manager",severity:"os"},"ctrl+alt+del":{action:"Security screen",severity:"os"},"alt+tab":{action:"Switch windows",severity:"os"},"alt+f4":{action:"Close active window",severity:"os"}}},linux:{browsers:{firefox:"$ref:platforms.windows.browsers.firefox",chrome:"$ref:platforms.windows.browsers.chrome",edge:"$ref:platforms.windows.browsers.edge"},system:{$comment:"GNOME defaults. KDE and other DEs have similar-shaped reservations but vary; this is a conservative baseline.","meta+tab":{action:"Switch windows",severity:"os"},"meta+a":{action:"Show applications",severity:"os"},"meta+l":{action:"Lock screen",severity:"os"},"meta+v":{action:"Notification list",severity:"os"},"alt+tab":{action:"Switch applications",severity:"os"},"alt+esc":{action:"Cycle windows",severity:"os"},"alt+f2":{action:"Run command",severity:"os"},"alt+f4":{action:"Close window",severity:"os"},"ctrl+alt+tab":{action:"Top-bar accessibility switcher",severity:"os"},"ctrl+alt+del":{action:"Power-off dialog",severity:"os"},"ctrl+shift+alt+r":{action:"Toggle screencast recording",severity:"os"}},noteForCtrlAlt:"Some Linux setups map right-Alt (AltGr) to 'compose' for typing accented characters, in which case AltGr+letter is reported as ctrl+alt+letter and types a special character. Avoid the ctrl+alt+letter namespace on Linux."}}};var ht="1.0",ft=new Set(["hard","os","menu-activation"]);function yt(t=typeof navigator<"u"?navigator:null){if(!t)return null;let e=t.platform||"";if(/Mac|iPhone|iPad|iPod/.test(e))return"mac";if(/Win/i.test(e))return"windows";if(/Linux|X11/i.test(e))return"linux";let r=t.userAgent||"";return/Mac OS X/i.test(r)?"mac":/Windows/i.test(r)?"windows":/Linux|X11/i.test(r)?"linux":null}function mt(t=typeof navigator<"u"?navigator:null){if(!t)return null;let e=t.userAgent||"";return/Edg\//.test(e)?"edge":/Firefox/.test(e)?"firefox":/Chrome/.test(e)?"chrome":/Safari/.test(e)?"safari":null}function ut(t){let e=String(t).toLowerCase().split("+").map(o=>o.trim()).filter(Boolean);if(!e.length)return"";let r=e[e.length-1],a=new Set(e.slice(0,-1)),i=[];return a.has("ctrl")&&i.push("ctrl"),a.has("meta")&&i.push("meta"),a.has("alt")&&i.push("alt"),a.has("shift")&&i.push("shift"),i.push(r),i.join("+")}function vt(t,e){if(typeof t!="string"||!t.startsWith("$ref:"))return t;let r=t.slice(5).split("."),a=e;for(let i of r)a=a?.[i];return a}function K(t){if(!t||typeof t!="object")return null;let e=Object.create(null);for(let[r,a]of Object.entries(t))!a||typeof a!="object"||!("severity"in a)||(e[ut(r)]=a);return e}function pt(t){let e={platforms:{}},r=t?.platforms||{};for(let[a,i]of Object.entries(r)){let o={};for(let[n,s]of Object.entries(i.browsers||{}))o[n]=K(vt(s,t));e.platforms[a]={browsers:o,system:K(i.system)}}return e}var N=g?.version?.schema===ht,wt=N?pt(g):{platforms:{}};function gt(t){if(!t)return null;let e=/^Key([A-Za-z])$/.exec(t);if(e)return e[1].toLowerCase();let r=/^Digit(\d)$/.exec(t);return r?r[1]:t.toLowerCase()}function bt(t){if(!t||t.bareModifier)return null;let e=t.code?gt(t.code):t.key;if(!e)return null;let r=[];return t.ctrl&&r.push("ctrl"),t.meta&&r.push("meta"),t.alt&&r.push("alt"),t.shift&&r.push("shift"),r.push(e),r.join("+")}function kt(t,{platform:e,browser:r}={}){if(!N)return null;let a=bt(t);if(!a)return null;let i=e&&wt.platforms[e];if(!i)return null;let n=(r?i.browsers?.[r]:null)?.[a];if(n)return{source:"browser",platform:e,browser:r,lookupKey:a,action:n.action,severity:n.severity};let s=i.system?.[a];return s?{source:"system",platform:e,browser:r,lookupKey:a,action:s.action,severity:s.severity}:null}var St={mac:"macOS",windows:"Windows",linux:"Linux"};function L(t){return ft.has(t)}function xt(t,e){let r=St[t.platform]||t.platform,a=t.source==="browser"?`${t.browser} on ${r}`:`${r}`,i=L(t.severity)?"will not fire":"fires in normal browsing";return`[hotkey-router] "${e}" (${t.lookupKey}) reserved by ${a}: "${t.action}" [${t.severity}] \u2014 ${i}.`}function Ct(t,e,r=console){let a=xt(t,e);(L(t.severity)?r.warn:r.info).call(r,a)}function $(t,e={}){if(!t||typeof t.onBind!="function")throw new TypeError("installReservationWarnings(hotkeys) requires a hotkey-router instance with onBind() (>= v0.2.0)");let r=e.platform!==void 0?e.platform:yt(),a=e.browser!==void 0?e.browser:mt(),i=e.console||console;return t.onBind(({combo:o,raw:n,options:s})=>{if(s?.warnOnReserved===!1||!r||!a)return;let l=kt(o,{platform:r,browser:a});l&&Ct(l,n,i)})}var Ot=g?.version?.schema,Rt=g?.version?.generated;$(O);var Dt=O;export{Dt as default};
|
|
3
|
+
function R(t,e,r,a){return t.addEventListener(e,r,a),()=>t.removeEventListener(e,r,a)}var W=typeof navigator<"u"&&/Mac|iPhone|iPad|iPod/.test(navigator.platform),H=new Set(["ctrl","shift","alt","meta"]),U=Object.freeze({Alt:"alt",AltGraph:"alt",Control:"ctrl",Shift:"shift",Meta:"meta",OS:"meta"}),V=new Map([["^","ctrl"],["!","alt"],["+","shift"],["#","meta"]]),A=Object.freeze({esc:"escape",escape:"escape",space:" ",spacebar:" ",enter:"enter",return:"enter",del:"delete",delete:"delete",backspace:"backspace",tab:"tab",up:"arrowup",down:"arrowdown",left:"arrowleft",right:"arrowright",home:"home",end:"end",pageup:"pageup",pagedown:"pagedown",pgup:"pageup",pgdn:"pagedown",cmd:"meta",command:"meta",win:"meta",meta:"meta",control:"ctrl",ctrl:"ctrl",option:"alt",alt:"alt",shift:"shift","\u2318":"meta","\u2303":"ctrl","\u2325":"alt","\u21E7":"shift",mod:W?"meta":"ctrl",plus:"+"}),M=!0,C=typeof window<"u"?window:null,p=[],u=new Map,w=new Map,v=new Map,E=!1,q=1;function F(t){let e=String(t??"");if(e===" ")return" ";let a=e.trim().toLowerCase();return A[a]??a}function S({ctrl:t,shift:e,alt:r,meta:a,key:i,code:o,bareModifier:n}){if(n)return`__bare__:${n}`;let s="";return t&&(s+="ctrl+"),e&&(s+="shift+"),r&&(s+="alt+"),a&&(s+="meta+"),o?s+"code:"+o:s?s+i:i}function G(t){let e=U[t.key];return e?`__bare__:${e}`:S({ctrl:t.ctrlKey,shift:t.shiftKey,alt:t.altKey,meta:t.metaKey,key:F(t.key)})}function X(t){if(!t.code)return null;let e="";return t.ctrlKey&&(e+="ctrl+"),t.shiftKey&&(e+="shift+"),t.altKey&&(e+="alt+"),t.metaKey&&(e+="meta+"),e+"code:"+t.code}function Z(t){let e=F(t);return H.has(e)}var Q=/(^|\+)code:([A-Za-z][A-Za-z0-9_]*)/;function P(t){let e=String(t||"").trim(),r=/\s+up$/i.test(e),a=r?e.replace(/\s+up$/i,"").trim():e,i={ctrl:!1,shift:!1,alt:!1,meta:!1,key:null,code:null,bareModifier:null},o=a;for(;o.length;){let c=V.get(o[0])??null;if(!c)break;i[c]=!0,o=o.slice(1).trimStart()}let n=o.match(/code:[A-Za-z][A-Za-z0-9_]*/g);if(n&&n.length>1)throw new Error(`Invalid hotkey "${t}": multiple code: tokens not supported (got ${n.length}).`);let s=null,d=o.replace(Q,(c,y,j)=>(s=j,y+"__code__")).toLowerCase(),h=d.split("+").map(c=>c.trim()),m=h.filter(Boolean),f=h.some(c=>c==="")&&d.includes("++");if(!f){let c=!1;for(let y of m)if(Z(y)){if(c)throw new Error(`Invalid hotkey "${t}": modifiers must come before the base key (e.g. "ctrl+k", not "k+ctrl")`)}else c||(c=!0)}for(let c of m){if(c==="__code__"){i.code=s;continue}let y=A[c]??c;y==="ctrl"?i.ctrl=!0:y==="shift"?i.shift=!0:y==="alt"?i.alt=!0:y==="meta"?i.meta=!0:i.key=F(y)}if(!i.key&&!i.code&&f&&(i.key="+"),!i.key&&!i.code){let c=(i.ctrl?1:0)+(i.shift?1:0)+(i.alt?1:0)+(i.meta?1:0);if(c===1)i.bareModifier=i.ctrl?"ctrl":i.shift?"shift":i.alt?"alt":"meta";else throw c>1?new Error(`Invalid hotkey "${t}": multi-modifier bare bindings not supported. Add a base key (e.g. "ctrl+alt+x").`):new Error(`Invalid hotkey "${t}" (missing base key)`)}return{combo:i,type:r?"keyup":"keydown",raw:t}}function Y(t){let e=t?.target;if(!e)return!1;let r=String(e.tagName||"").toUpperCase();return e.isContentEditable||r==="INPUT"||r==="TEXTAREA"||r==="SELECT"||e.getAttribute?.("role")==="textbox"}function J(t,e){let r=e?{...e}:{};return t==="keydown"&&r.repeat==null&&(r.repeat=!1),r.priority==null&&(r.priority=0),r}function tt(t,e){let r=u.get(t);r||(r=new Map,u.set(t,r));let a=r.get(e);return a||(a=[],r.set(e,a)),a}function x(t){let e=v.get(t);if(!e)return!1;let{comboStr:r,type:a}=e,i=u.get(r),o=i?.get(a);if(!o?.length)return v.delete(t),!1;let n=o.filter(s=>s.id!==t);return n.length?i.set(a,n):i.delete(a),i.size||u.delete(r),v.delete(t),!0}function D(t,e,r=null,a={}){let{combo:i,type:o,raw:n}=P(t),s=S(i),l=J(o,a),d={id:q++,comboStr:s,type:o,combo:i,handler:e,plugin:r,raw:n,options:l};if(tt(s,o).push(d),v.set(d.id,{comboStr:s,type:o}),p.length)for(let m of p)try{m({combo:i,raw:n,options:l,plugin:r,id:d.id})}catch(f){console.error("[hotkey-router] onBind hook threw:",f)}let h=()=>x(d.id);return h.id=d.id,h.hotkey=n,h}function et(t){if(typeof t!="function")throw new TypeError("hotkeys.onBind(hook) expects a function");return p.push(t),()=>{p=p.filter(e=>e!==t)}}function rt(t,e){let{combo:r,type:a}=P(t),i=S(r),o=u.get(i);if(!o)return;let n=o.get(a);if(n?.length){if(e){let s=[];for(let l of n)l.handler===e?v.delete(l.id):s.push(l);s.length?o.set(a,s):o.delete(a)}else{for(let s of n)v.delete(s.id);o.delete(a)}o.size||u.delete(i)}}function it(t,e){if(w.has(t))throw new Error(`Plugin "${t}" already registered`);let r=[];for(let[a,i]of Object.entries(e)){let[o,n]=Array.isArray(i)?i:[i,{}],s=D(a,o,t,n);r.push(s.id)}return w.set(t,r),()=>I(t)}function I(t){let e=w.get(t);if(e){for(let r of e)x(r);w.delete(t)}}function at(){E=!0}function ot(){E=!1}function st(t=!0){M=!!t}function nt(t){C=t}function B(t){let e=null;for(let r of t)e?(r.options.priority>e.options.priority||r.options.priority===e.options.priority&&r.id>e.id)&&(e=r):e=r;return e}function T(t){return e=>{if(E)return;let r=M&&Y(e),a=G(e),i=X(e),o=new Set,n=[];for(let d of[a,i]){if(!d||o.has(d))continue;o.add(d);let h=u.get(d)?.get(t);if(h?.length)for(let m of h){let f=m.options;r&&!f.allowIn?.(e)||t==="keydown"&&f.repeat===!1&&e.repeat||f.when&&f.when(e)===!1||n.push(m)}}if(!n.length)return;let s=B(n),l=s.options;l.preventDefault&&e.preventDefault(),l.stopImmediatePropagation?e.stopImmediatePropagation():l.stopPropagation&&e.stopPropagation(),s.handler(e),l.once&&x(s.id)}}var b=null,k=null;function _({target:t=C,capture:e=!1}={}){if(!t)throw new Error("hotkeys.init() requires a target (e.g. window)");(b||k)&&z(),b=R(t,"keydown",T("keydown"),{capture:e}),k=R(t,"keyup",T("keyup"),{capture:e})}function z(){b?.(),k?.(),b=null,k=null,u.clear(),w.clear(),v.clear()}var ct={ctrl:"Control",shift:"Shift",alt:"Alt",meta:"Meta"};function lt(t,{type:e}={}){let{combo:r,type:a}=P(t),i=S(r),o=e||a,n=u.get(i)?.get(o);if(!n?.length)return!1;let s=r.bareModifier?ct[r.bareModifier]:r.key,l=!!r.alt,d=!!r.ctrl,h=!!r.shift,m=!!r.meta;r.bareModifier&&o==="keyup"&&(r.bareModifier==="alt"?l=!1:r.bareModifier==="ctrl"?d=!1:r.bareModifier==="shift"?h=!1:r.bareModifier==="meta"&&(m=!1));let f={type:o,key:s,code:r.code||null,ctrlKey:d,shiftKey:h,altKey:l,metaKey:m,repeat:!1,preventDefault(){},stopPropagation(){},stopImmediatePropagation(){},target:null},c=B(n.filter(y=>y.options.when?y.options.when(f)!==!1:!0));return c?(c.handler(f),c.options.once&&x(c.id),!0):!1}C&&_();var O={init:_,destroy:z,setTarget:nt,bind:D,unbind:rt,registerPlugin:it,unregisterPlugin:I,onBind:et,pause:at,resume:ot,ignoreInput:st,trigger:lt};var g={$comment:"Machine-readable companion to docs/browser-hotkeys.md. Each combo entry describes who claims the binding and how strongly. Intended for runtime conflict warnings in keyboard-routing libraries (e.g. hotkey-router) and dev-time validation in apps like WebRadar. Update both this file and the markdown together \u2014 the markdown is the human source of truth, this file is the machine projection.",version:{generated:"2026-05-17",schema:"1.0",sources:["https://support.mozilla.org/en-US/kb/keyboard-shortcuts-perform-firefox-tasks-quickly","https://firefox-source-docs.mozilla.org/devtools-user/keyboard_shortcuts/index.html","https://support.google.com/chrome/answer/157179","https://developer.chrome.com/docs/devtools/shortcuts","https://support.apple.com/guide/safari/keyboard-shortcuts-and-gestures-cpsh003/mac","https://developer.apple.com/documentation/safari-developer-tools/web-inspector","https://support.microsoft.com/en-us/microsoft-edge/keyboard-shortcuts-in-microsoft-edge-50d3edab-30d9-c7e4-21ce-37fe2713cfad","https://support.apple.com/en-us/102650","https://support.microsoft.com/en-us/windows/keyboard-shortcuts-in-windows-dcc61a57-8ff0-cffe-9796-cb9706c75eec","https://help.gnome.org/gnome-help/shell-keyboard-shortcuts.html"]},severityLevels:{hard:"Browser or OS intercepts before page-world keydown fires. preventDefault() can't recover it.","find-bar-only":"Reserved only when the browser's find bar has focus. Fires in page world during normal browsing.","menu-activation":"Win/Linux Alt+letter activates the browser menu bar. Hard to suppress reliably.","system-text":"Only reserved while a text input has focus (terminal-style cursor controls on macOS).",compose:"macOS Option+letter types a special character in text inputs. Outside inputs, free for page world.",os:"Operating system intercepts globally, before any application sees it.","devtools-open":"Only relevant when DevTools is already open; outside DevTools it fires normally."},$keyFormat:"Each combo key uses hotkey-router-compatible syntax: modifiers in canonical order (ctrl|meta|alt|shift) joined by '+', then the key (lowercased letter, or symbolic name). Per-platform tables use the platform's native modifier name \u2014 `meta` on macOS = Cmd, `meta` on Windows = Win, `ctrl` on Linux/Windows = the primary modifier.",platforms:{mac:{browsers:{firefox:{"meta+a":{action:"Select all",severity:"hard"},"meta+b":{action:"Bookmarks sidebar",severity:"hard"},"meta+c":{action:"Copy",severity:"hard"},"meta+d":{action:"Bookmark this page",severity:"hard"},"meta+e":{action:"Search from page",severity:"hard"},"meta+f":{action:"Find in page",severity:"hard"},"meta+g":{action:"Find next",severity:"hard"},"meta+j":{action:"Downloads",severity:"hard"},"meta+l":{action:"Focus address bar",severity:"hard"},"meta+n":{action:"New window",severity:"hard"},"meta+o":{action:"Open file",severity:"hard"},"meta+p":{action:"Print",severity:"hard"},"meta+r":{action:"Reload",severity:"hard"},"meta+s":{action:"Save page",severity:"hard"},"meta+t":{action:"New tab",severity:"hard"},"meta+u":{action:"View source",severity:"hard"},"meta+v":{action:"Paste",severity:"hard"},"meta+w":{action:"Close tab",severity:"hard"},"meta+x":{action:"Cut",severity:"hard"},"meta+y":{action:"Redo",severity:"hard"},"meta+z":{action:"Undo",severity:"hard"},"meta+shift+a":{action:"Add-ons manager",severity:"hard"},"meta+shift+b":{action:"Bookmarks toolbar",severity:"hard"},"meta+shift+c":{action:"Inspector (element picker)",severity:"hard"},"meta+shift+d":{action:"Bookmark all tabs",severity:"hard"},"meta+shift+e":{action:"Network Monitor",severity:"hard"},"meta+shift+f":{action:"Toggle fullscreen",severity:"hard"},"meta+shift+g":{action:"Find previous",severity:"find-bar-only"},"meta+shift+j":{action:"Browser Console",severity:"hard"},"meta+shift+n":{action:"New private window",severity:"hard"},"meta+shift+o":{action:"Bookmarks library",severity:"hard"},"meta+shift+p":{action:"New private window (alt)",severity:"hard"},"meta+shift+q":{action:"Quit (with confirm)",severity:"hard"},"meta+shift+s":{action:"Firefox Screenshot",severity:"hard"},"meta+shift+t":{action:"Reopen closed tab",severity:"hard"},"meta+shift+v":{action:"Paste as plain text",severity:"hard"},"meta+shift+w":{action:"Close window",severity:"hard"},"meta+shift+z":{action:"Redo",severity:"hard"},"meta+alt+c":{action:"Inspector",severity:"hard"},"meta+alt+e":{action:"Network Monitor",severity:"hard"},"meta+alt+i":{action:"DevTools Toolbox",severity:"hard"},"meta+alt+j":{action:"Browser Console",severity:"hard"},"meta+alt+k":{action:"Web Console",severity:"hard"},"meta+alt+m":{action:"Responsive Design Mode",severity:"hard"},"meta+alt+z":{action:"Debugger",severity:"hard"}},chrome:{"meta+a":{action:"Select all",severity:"hard"},"meta+c":{action:"Copy",severity:"hard"},"meta+d":{action:"Bookmark this page",severity:"hard"},"meta+e":{action:"Search selected text",severity:"find-bar-only"},"meta+f":{action:"Find in page",severity:"hard"},"meta+g":{action:"Find next",severity:"hard"},"meta+l":{action:"Focus address bar",severity:"hard"},"meta+n":{action:"New window",severity:"hard"},"meta+o":{action:"Open file",severity:"hard"},"meta+p":{action:"Print",severity:"hard"},"meta+r":{action:"Reload",severity:"hard"},"meta+s":{action:"Save page",severity:"hard"},"meta+t":{action:"New tab",severity:"hard"},"meta+v":{action:"Paste",severity:"hard"},"meta+w":{action:"Close tab",severity:"hard"},"meta+x":{action:"Cut",severity:"hard"},"meta+y":{action:"History page",severity:"hard"},"meta+z":{action:"Undo",severity:"hard"},"meta+shift+b":{action:"Bookmarks bar toggle",severity:"hard"},"meta+shift+c":{action:"DevTools Inspector",severity:"hard"},"meta+shift+d":{action:"Bookmark all tabs",severity:"hard"},"meta+shift+g":{action:"Find previous",severity:"find-bar-only"},"meta+shift+j":{action:"Downloads page",severity:"hard"},"meta+shift+m":{action:"Switch user / guest",severity:"hard"},"meta+shift+n":{action:"New incognito window",severity:"hard"},"meta+shift+r":{action:"Hard reload",severity:"hard"},"meta+shift+t":{action:"Reopen closed tab",severity:"hard"},"meta+shift+v":{action:"Paste as plain text",severity:"hard"},"meta+shift+w":{action:"Close window",severity:"hard"},"meta+shift+z":{action:"Redo",severity:"hard"},"meta+alt+i":{action:"DevTools",severity:"hard"},"meta+alt+j":{action:"DevTools (console)",severity:"hard"},"meta+alt+u":{action:"View source",severity:"hard"}},safari:{"meta+a":{action:"Select all",severity:"hard"},"meta+c":{action:"Copy",severity:"hard"},"meta+d":{action:"Add to Reading List",severity:"hard"},"meta+f":{action:"Find in page",severity:"hard"},"meta+g":{action:"Find next",severity:"hard"},"meta+j":{action:"Downloads",severity:"hard"},"meta+l":{action:"Focus Smart Search",severity:"hard"},"meta+n":{action:"New window",severity:"hard"},"meta+p":{action:"Print",severity:"hard"},"meta+r":{action:"Reload",severity:"hard"},"meta+s":{action:"Save page",severity:"hard"},"meta+t":{action:"New tab",severity:"hard"},"meta+v":{action:"Paste",severity:"hard"},"meta+w":{action:"Close tab",severity:"hard"},"meta+x":{action:"Cut",severity:"hard"},"meta+z":{action:"Undo",severity:"hard"},"meta+shift+c":{action:"Inspector pick element",severity:"hard"},"meta+shift+d":{action:"Add Reading List item",severity:"hard"},"meta+shift+g":{action:"Find previous",severity:"find-bar-only"},"meta+shift+h":{action:"Go to homepage",severity:"hard"},"meta+shift+n":{action:"New private window",severity:"hard"},"meta+shift+r":{action:"Reader mode",severity:"hard"},"meta+shift+t":{action:"Reopen last closed tab",severity:"hard"},"meta+shift+v":{action:"Paste as plain text",severity:"hard"},"meta+shift+w":{action:"Close window",severity:"hard"},"meta+shift+z":{action:"Redo",severity:"hard"},"meta+alt+c":{action:"Web Inspector pick",severity:"hard"},"meta+alt+i":{action:"Web Inspector",severity:"hard"}}},system:{"meta+space":{action:"Spotlight",severity:"os"},"meta+tab":{action:"App switcher",severity:"os"},"meta+h":{action:"Hide app",severity:"os"},"meta+m":{action:"Minimize window",severity:"os"},"meta+q":{action:"Quit app",severity:"os"},"meta+shift+3":{action:"Screenshot full screen",severity:"os"},"meta+shift+4":{action:"Screenshot selection",severity:"os"},"meta+shift+5":{action:"Screen recording tool",severity:"os"},"meta+shift+q":{action:"Log out",severity:"os"},"meta+ctrl+q":{action:"Lock screen",severity:"os"},"meta+ctrl+space":{action:"Character Viewer",severity:"os"},"meta+alt+esc":{action:"Force-quit dialog",severity:"os"}},noteForLetterCombos:{"alt+a..z":{behavior:"compose",note:"Option+letter types special characters in text inputs (Option+E\u2192\xB4, Option+A\u2192\xE5, Option+F\u2192\u0192). Outside inputs, free for page world."},"ctrl+a..z":{behavior:"system-text",note:"Mac uses Ctrl+letter as terminal-style cursor controls inside text inputs (Ctrl+A=line start, Ctrl+E=line end, Ctrl+K=delete-to-end, etc.). Outside inputs, free for page world."}}},windows:{browsers:{firefox:{"ctrl+a":{action:"Select all",severity:"hard"},"ctrl+b":{action:"Bookmarks sidebar",severity:"hard"},"ctrl+c":{action:"Copy",severity:"hard"},"ctrl+d":{action:"Bookmark this page",severity:"hard"},"ctrl+f":{action:"Find in page",severity:"hard"},"ctrl+g":{action:"Find next",severity:"hard"},"ctrl+h":{action:"History sidebar",severity:"hard"},"ctrl+j":{action:"Downloads",severity:"hard"},"ctrl+l":{action:"Focus address bar",severity:"hard"},"ctrl+n":{action:"New window",severity:"hard"},"ctrl+o":{action:"Open file",severity:"hard"},"ctrl+p":{action:"Print",severity:"hard"},"ctrl+r":{action:"Reload",severity:"hard"},"ctrl+s":{action:"Save page",severity:"hard"},"ctrl+t":{action:"New tab",severity:"hard"},"ctrl+u":{action:"View source",severity:"hard"},"ctrl+v":{action:"Paste",severity:"hard"},"ctrl+w":{action:"Close tab",severity:"hard"},"ctrl+x":{action:"Cut",severity:"hard"},"ctrl+y":{action:"Redo",severity:"hard"},"ctrl+z":{action:"Undo",severity:"hard"},"ctrl+shift+a":{action:"Add-ons manager",severity:"hard"},"ctrl+shift+b":{action:"Bookmarks toolbar",severity:"hard"},"ctrl+shift+c":{action:"Inspector (element picker)",severity:"hard"},"ctrl+shift+d":{action:"Bookmark all tabs",severity:"hard"},"ctrl+shift+e":{action:"Network Monitor",severity:"hard"},"ctrl+shift+g":{action:"Find previous",severity:"find-bar-only"},"ctrl+shift+i":{action:"Developer Tools",severity:"hard"},"ctrl+shift+j":{action:"Browser Console",severity:"hard"},"ctrl+shift+k":{action:"Web Console",severity:"hard"},"ctrl+shift+m":{action:"Responsive Design Mode",severity:"hard"},"ctrl+shift+n":{action:"New private window",severity:"hard"},"ctrl+shift+o":{action:"Bookmarks library",severity:"hard"},"ctrl+shift+p":{action:"New private window (alt)",severity:"hard"},"ctrl+shift+s":{action:"Firefox Screenshot",severity:"hard"},"ctrl+shift+t":{action:"Reopen closed tab",severity:"hard"},"ctrl+shift+v":{action:"Paste as plain text",severity:"hard"},"ctrl+shift+w":{action:"Close window",severity:"hard"},"ctrl+shift+y":{action:"Downloads (alt)",severity:"hard"},"ctrl+shift+z":{action:"Debugger",severity:"hard"},"alt+f":{action:"File menu",severity:"menu-activation"},"alt+e":{action:"Edit menu",severity:"menu-activation"},"alt+v":{action:"View menu",severity:"menu-activation"},"alt+b":{action:"Bookmarks menu",severity:"menu-activation"},"alt+t":{action:"Tools menu",severity:"menu-activation"},"alt+h":{action:"Help menu",severity:"menu-activation"},"alt+d":{action:"Focus address bar",severity:"hard"}},chrome:{"ctrl+a":{action:"Select all",severity:"hard"},"ctrl+c":{action:"Copy",severity:"hard"},"ctrl+d":{action:"Bookmark this page",severity:"hard"},"ctrl+e":{action:"Search from page",severity:"hard"},"ctrl+f":{action:"Find in page",severity:"hard"},"ctrl+g":{action:"Find next",severity:"hard"},"ctrl+h":{action:"History page",severity:"hard"},"ctrl+j":{action:"Downloads",severity:"hard"},"ctrl+k":{action:"Search from page",severity:"hard"},"ctrl+l":{action:"Focus address bar",severity:"hard"},"ctrl+n":{action:"New window",severity:"hard"},"ctrl+o":{action:"Open file",severity:"hard"},"ctrl+p":{action:"Print",severity:"hard"},"ctrl+r":{action:"Reload",severity:"hard"},"ctrl+s":{action:"Save page",severity:"hard"},"ctrl+t":{action:"New tab",severity:"hard"},"ctrl+u":{action:"View source",severity:"hard"},"ctrl+v":{action:"Paste",severity:"hard"},"ctrl+w":{action:"Close tab",severity:"hard"},"ctrl+x":{action:"Cut",severity:"hard"},"ctrl+z":{action:"Undo",severity:"hard"},"ctrl+shift+b":{action:"Bookmarks bar toggle",severity:"hard"},"ctrl+shift+d":{action:"Bookmark all tabs",severity:"hard"},"ctrl+shift+g":{action:"Find previous",severity:"find-bar-only"},"ctrl+shift+i":{action:"DevTools",severity:"hard"},"ctrl+shift+j":{action:"DevTools (console)",severity:"hard"},"ctrl+shift+m":{action:"Switch user / guest",severity:"hard"},"ctrl+shift+n":{action:"Incognito window",severity:"hard"},"ctrl+shift+r":{action:"Hard reload",severity:"hard"},"ctrl+shift+t":{action:"Reopen closed tab",severity:"hard"},"ctrl+shift+v":{action:"Paste as plain text",severity:"hard"},"ctrl+shift+w":{action:"Close window",severity:"hard"},"alt+d":{action:"Focus address bar",severity:"hard"},"alt+e":{action:"Customize menu",severity:"menu-activation"},"alt+f":{action:"Customize menu (alt)",severity:"menu-activation"}},edge:{"ctrl+a":{action:"Select all",severity:"hard"},"ctrl+c":{action:"Copy",severity:"hard"},"ctrl+d":{action:"Favorite this page",severity:"hard"},"ctrl+e":{action:"Search from page",severity:"hard"},"ctrl+f":{action:"Find in page",severity:"hard"},"ctrl+g":{action:"Find next",severity:"hard"},"ctrl+h":{action:"History",severity:"hard"},"ctrl+j":{action:"Downloads",severity:"hard"},"ctrl+k":{action:"Search from page",severity:"hard"},"ctrl+l":{action:"Focus address bar",severity:"hard"},"ctrl+m":{action:"Mute current tab",severity:"hard"},"ctrl+n":{action:"New window",severity:"hard"},"ctrl+o":{action:"Open file",severity:"hard"},"ctrl+p":{action:"Print",severity:"hard"},"ctrl+r":{action:"Reload",severity:"hard"},"ctrl+s":{action:"Save page",severity:"hard"},"ctrl+t":{action:"New tab",severity:"hard"},"ctrl+u":{action:"View source",severity:"hard"},"ctrl+v":{action:"Paste",severity:"hard"},"ctrl+w":{action:"Close tab",severity:"hard"},"ctrl+x":{action:"Cut",severity:"hard"},"ctrl+z":{action:"Undo",severity:"hard"},"ctrl+shift+b":{action:"Favorites bar toggle",severity:"hard"},"ctrl+shift+d":{action:"Bookmark all tabs in folder",severity:"hard"},"ctrl+shift+e":{action:"Search in sidebar",severity:"hard"},"ctrl+shift+g":{action:"Find previous",severity:"find-bar-only"},"ctrl+shift+i":{action:"DevTools",severity:"hard"},"ctrl+shift+j":{action:"DevTools (console)",severity:"hard"},"ctrl+shift+k":{action:"Duplicate tab",severity:"hard"},"ctrl+shift+l":{action:"Paste and search",severity:"hard"},"ctrl+shift+m":{action:"Switch user / guest",severity:"hard"},"ctrl+shift+n":{action:"InPrivate window",severity:"hard"},"ctrl+shift+o":{action:"Open favorites",severity:"hard"},"ctrl+shift+p":{action:"Print (system dialog)",severity:"hard"},"ctrl+shift+r":{action:"Hard reload",severity:"hard"},"ctrl+shift+t":{action:"Reopen closed tab",severity:"hard"},"ctrl+shift+u":{action:"Read Aloud toggle",severity:"hard"},"ctrl+shift+v":{action:"Paste without formatting",severity:"hard"},"ctrl+shift+w":{action:"Close window",severity:"hard"},"ctrl+shift+y":{action:"Collections",severity:"hard"},"alt+d":{action:"Focus address bar",severity:"hard"},"alt+e":{action:"Settings menu",severity:"menu-activation"},"alt+f":{action:"Settings menu (alt)",severity:"menu-activation"},"alt+shift+b":{action:"Focus favorites bar",severity:"hard"},"alt+shift+i":{action:"Send feedback dialog",severity:"hard"},"alt+shift+t":{action:"Focus app bar toolbar",severity:"hard"}}},system:{"meta+a":{action:"Action Center",severity:"os"},"meta+c":{action:"Windows search",severity:"os"},"meta+d":{action:"Show desktop",severity:"os"},"meta+e":{action:"File Explorer",severity:"os"},"meta+g":{action:"Game bar",severity:"os"},"meta+h":{action:"Voice dictation",severity:"os"},"meta+i":{action:"Settings",severity:"os"},"meta+l":{action:"Lock",severity:"os"},"meta+n":{action:"Notification center",severity:"os"},"meta+p":{action:"Project / display mode",severity:"os"},"meta+r":{action:"Run dialog",severity:"os"},"meta+s":{action:"Search",severity:"os"},"meta+v":{action:"Clipboard history",severity:"os"},"meta+w":{action:"Widgets",severity:"os"},"meta+x":{action:"Quick Link menu",severity:"os"},"meta+z":{action:"Snap layouts",severity:"os"},"meta+tab":{action:"Task View",severity:"os"},"meta+shift+s":{action:"Screenshot region",severity:"os"},"ctrl+shift+esc":{action:"Task Manager",severity:"os"},"ctrl+alt+del":{action:"Security screen",severity:"os"},"alt+tab":{action:"Switch windows",severity:"os"},"alt+f4":{action:"Close active window",severity:"os"}}},linux:{browsers:{firefox:"$ref:platforms.windows.browsers.firefox",chrome:"$ref:platforms.windows.browsers.chrome",edge:"$ref:platforms.windows.browsers.edge"},system:{$comment:"GNOME defaults. KDE and other DEs have similar-shaped reservations but vary; this is a conservative baseline.","meta+tab":{action:"Switch windows",severity:"os"},"meta+a":{action:"Show applications",severity:"os"},"meta+l":{action:"Lock screen",severity:"os"},"meta+v":{action:"Notification list",severity:"os"},"alt+tab":{action:"Switch applications",severity:"os"},"alt+esc":{action:"Cycle windows",severity:"os"},"alt+f2":{action:"Run command",severity:"os"},"alt+f4":{action:"Close window",severity:"os"},"ctrl+alt+tab":{action:"Top-bar accessibility switcher",severity:"os"},"ctrl+alt+del":{action:"Power-off dialog",severity:"os"},"ctrl+shift+alt+r":{action:"Toggle screencast recording",severity:"os"}},noteForCtrlAlt:"Some Linux setups map right-Alt (AltGr) to 'compose' for typing accented characters, in which case AltGr+letter is reported as ctrl+alt+letter and types a special character. Avoid the ctrl+alt+letter namespace on Linux."}}};var ht="1.0",ft=new Set(["hard","os","menu-activation"]);function yt(t=typeof navigator<"u"?navigator:null){if(!t)return null;let e=t.platform||"";if(/Mac|iPhone|iPad|iPod/.test(e))return"mac";if(/Win/i.test(e))return"windows";if(/Linux|X11/i.test(e))return"linux";let r=t.userAgent||"";return/Mac OS X/i.test(r)?"mac":/Windows/i.test(r)?"windows":/Linux|X11/i.test(r)?"linux":null}function mt(t=typeof navigator<"u"?navigator:null){if(!t)return null;let e=t.userAgent||"";return/Edg\//.test(e)?"edge":/Firefox/.test(e)?"firefox":/Chrome/.test(e)?"chrome":/Safari/.test(e)?"safari":null}function ut(t){let e=String(t).toLowerCase().split("+").map(o=>o.trim()).filter(Boolean);if(!e.length)return"";let r=e[e.length-1],a=new Set(e.slice(0,-1)),i=[];return a.has("ctrl")&&i.push("ctrl"),a.has("meta")&&i.push("meta"),a.has("alt")&&i.push("alt"),a.has("shift")&&i.push("shift"),i.push(r),i.join("+")}function vt(t,e){if(typeof t!="string"||!t.startsWith("$ref:"))return t;let r=t.slice(5).split("."),a=e;for(let i of r)a=a?.[i];return a}function K(t){if(!t||typeof t!="object")return null;let e=Object.create(null);for(let[r,a]of Object.entries(t))!a||typeof a!="object"||!("severity"in a)||(e[ut(r)]=a);return e}function pt(t){let e={platforms:{}},r=t?.platforms||{};for(let[a,i]of Object.entries(r)){let o={};for(let[n,s]of Object.entries(i.browsers||{}))o[n]=K(vt(s,t));e.platforms[a]={browsers:o,system:K(i.system)}}return e}var N=g?.version?.schema===ht,wt=N?pt(g):{platforms:{}};function gt(t){if(!t)return null;let e=/^Key([A-Za-z])$/.exec(t);if(e)return e[1].toLowerCase();let r=/^Digit(\d)$/.exec(t);return r?r[1]:t.toLowerCase()}function bt(t){if(!t||t.bareModifier)return null;let e=t.code?gt(t.code):t.key;if(!e)return null;let r=[];return t.ctrl&&r.push("ctrl"),t.meta&&r.push("meta"),t.alt&&r.push("alt"),t.shift&&r.push("shift"),r.push(e),r.join("+")}function kt(t,{platform:e,browser:r}={}){if(!N)return null;let a=bt(t);if(!a)return null;let i=e&&wt.platforms[e];if(!i)return null;let n=(r?i.browsers?.[r]:null)?.[a];if(n)return{source:"browser",platform:e,browser:r,lookupKey:a,action:n.action,severity:n.severity};let s=i.system?.[a];return s?{source:"system",platform:e,browser:r,lookupKey:a,action:s.action,severity:s.severity}:null}var St={mac:"macOS",windows:"Windows",linux:"Linux"};function L(t){return ft.has(t)}function xt(t,e){let r=St[t.platform]||t.platform,a=t.source==="browser"?`${t.browser} on ${r}`:`${r}`,i=L(t.severity)?"will not fire":"fires in normal browsing";return`[hotkey-router] "${e}" (${t.lookupKey}) reserved by ${a}: "${t.action}" [${t.severity}] \u2014 ${i}.`}function Ct(t,e,r=console){let a=xt(t,e);(L(t.severity)?r.warn:r.info).call(r,a)}function $(t,e={}){if(!t||typeof t.onBind!="function")throw new TypeError("installReservationWarnings(hotkeys) requires a hotkey-router instance with onBind() (>= v0.2.0)");let r=e.platform!==void 0?e.platform:yt(),a=e.browser!==void 0?e.browser:mt(),i=e.console||console;return t.onBind(({combo:o,raw:n,options:s})=>{if(s?.warnOnReserved===!1||!r||!a)return;let l=kt(o,{platform:r,browser:a});l&&Ct(l,n,i)})}var Ot=g?.version?.schema,Rt=g?.version?.generated;$(O);var Dt=O;export{Dt as default};
|
package/dist/hotkey-router.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// hotkey-router v0.2.
|
|
1
|
+
// hotkey-router v0.2.3 | SEE LICENSE IN LICENSE
|
|
2
2
|
|
|
3
3
|
var __defProp = Object.defineProperty;
|
|
4
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
@@ -101,9 +101,11 @@ var bindingIndex = /* @__PURE__ */ new Map();
|
|
|
101
101
|
var paused = false;
|
|
102
102
|
var nextId = 1;
|
|
103
103
|
function normalizeKey(key) {
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
104
|
+
const original = String(key ?? "");
|
|
105
|
+
if (original === " ") return " ";
|
|
106
|
+
const trimmed = original.trim();
|
|
107
|
+
const k = trimmed.toLowerCase();
|
|
108
|
+
return keyAliases[k] ?? k;
|
|
107
109
|
}
|
|
108
110
|
function comboKeyFromParts({ ctrl, shift, alt, meta, key, code, bareModifier }) {
|
|
109
111
|
if (bareModifier) return `__bare__:${bareModifier}`;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../hotkey-router.js"],
|
|
4
|
-
"sourcesContent": ["// hotkey-router.js\n// Hotkey Router \u2014 plugin-first keyboard shortcut router (tiny + predictable)\n//\n// Goals:\n// - O(1) dispatch by combo string lookup\n// - Deterministic routing: highest priority wins; ties -> most recently bound wins\n// - Safe plugin cleanup (never deletes other plugins\u2019 bindings)\n// - Modern browser target (KeyboardEvent.key + addEventListener)\n//\n// This core ships ZERO reservation data \u2014 opt in via `hotkey-router/auto`\n// or by calling `installReservationWarnings(hotkeys)` from the separate\n// `hotkey-router/reservations` module. Bundlers tree-shake the data file\n// out entirely when neither is imported.\n\n// --- tiny event helper ---\nfunction on(target, type, fn, options) {\n target.addEventListener(type, fn, options)\n return () => target.removeEventListener(type, fn, options)\n}\n\n// --- key alias map ---\nconst isMac =\n typeof navigator !== 'undefined' && /Mac|iPhone|iPad|iPod/.test(navigator.platform)\n\n\nconst modifierTokens = new Set(['ctrl', 'shift', 'alt', 'meta'])\n\n// Map from KeyboardEvent.key (which IS the modifier name on bare-modifier events)\n// to the canonical token used in combo strings. Used to detect bare-modifier\n// keydown/keyup events at dispatch time.\nconst MODIFIER_KEY_TOKEN = Object.freeze({\n Alt: 'alt',\n AltGraph: 'alt',\n Control: 'ctrl',\n Shift: 'shift',\n Meta: 'meta',\n OS: 'meta',\n})\n\nconst ahkPrefixMap = new Map([\n ['^', 'ctrl'], // Ctrl\n ['!', 'alt'], // Alt\n ['+', 'shift'], // Shift\n ['#', 'meta'], // Win / Command\n])\n\n\nconst keyAliases = Object.freeze({\n // common\n esc: 'escape',\n escape: 'escape',\n space: ' ',\n spacebar: ' ',\n enter: 'enter',\n return: 'enter',\n del: 'delete',\n delete: 'delete',\n backspace: 'backspace',\n tab: 'tab',\n\n // arrows\n up: 'arrowup',\n down: 'arrowdown',\n left: 'arrowleft',\n right: 'arrowright',\n\n // navigation\n home: 'home',\n end: 'end',\n pageup: 'pageup',\n pagedown: 'pagedown',\n pgup: 'pageup',\n pgdn: 'pagedown',\n\n // modifiers (words)\n cmd: 'meta',\n command: 'meta',\n win: 'meta',\n meta: 'meta',\n control: 'ctrl',\n ctrl: 'ctrl',\n option: 'alt',\n alt: 'alt',\n shift: 'shift',\n\n // modifiers (symbols)\n '\u2318': 'meta',\n '\u2303': 'ctrl',\n '\u2325': 'alt',\n '\u21E7': 'shift',\n\n // cross-platform \"mod\"\n mod: isMac ? 'meta' : 'ctrl',\n\n // plus key ergonomics\n plus: '+',\n})\n\n// --- config ---\nlet ignoreEditable = true\nlet defaultTarget = typeof window !== 'undefined' ? window : null\n\n// Bind hooks: subscribers called after each successful bind. Used by the\n// optional reservations module to emit conflict warnings without coupling\n// the core router to the (~5 KB) reservation table.\nlet bindHooks = []\n\n// --- state ---\n// comboStr -> Map<type, Array<Binding>>\nconst registry = new Map()\n// pluginName -> Array<bindingId>\nconst plugins = new Map()\n// bindingId -> { comboStr, type }\nconst bindingIndex = new Map()\n\nlet paused = false\nlet nextId = 1\n\n// --- helpers ---\nfunction normalizeKey(key) {\n const raw = String(key ?? '').trim()\n const k = raw.toLowerCase()\n return keyAliases[k] ?? (raw === ' ' ? ' ' : k)\n}\n\nfunction comboKeyFromParts({ ctrl, shift, alt, meta, key, code, bareModifier }) {\n // Bare-modifier bindings get a sentinel namespace so they can't collide with\n // regular chord combo strings (`'alt+x'` vs `'__bare__:alt'`).\n if (bareModifier) return `__bare__:${bareModifier}`\n\n let s = ''\n if (ctrl) s += 'ctrl+'\n if (shift) s += 'shift+'\n if (alt) s += 'alt+'\n if (meta) s += 'meta+'\n\n // Code-based combos use a `code:KeyX` suffix instead of the key character.\n if (code) return s + 'code:' + code\n return s ? s + key : key\n}\n\nfunction comboKeyFromEvent(e) {\n // If the event's `key` is itself a modifier name, this is a bare-modifier\n // press/release. Match it via the bare-modifier sentinel regardless of which\n // other modifier flags are set (loose match \u2014 Alt fires `alt` even when held\n // alongside Ctrl).\n const bareMod = MODIFIER_KEY_TOKEN[e.key]\n if (bareMod) return `__bare__:${bareMod}`\n\n return comboKeyFromParts({\n ctrl: e.ctrlKey,\n shift: e.shiftKey,\n alt: e.altKey,\n meta: e.metaKey,\n key: normalizeKey(e.key),\n })\n}\n\n// Code-based lookup string for an event. Independent of `comboKeyFromEvent`\n// because consumers can register either key-based OR code-based bindings (or\n// both) for the same physical key combo, and we look up under whichever they\n// chose.\nfunction codeKeyFromEvent(e) {\n if (!e.code) return null\n\n let s = ''\n if (e.ctrlKey) s += 'ctrl+'\n if (e.shiftKey) s += 'shift+'\n if (e.altKey) s += 'alt+'\n if (e.metaKey) s += 'meta+'\n return s + 'code:' + e.code\n}\n\n\nfunction isModifierToken(token) {\n const t = normalizeKey(token)\n return modifierTokens.has(t)\n}\n\n\n// Matches `code:KeyX` (case-sensitive value). Used to extract code tokens BEFORE\n// the rest of the parser lowercases everything, since KeyboardEvent.code values\n// are camelCase (KeyA, Digit1, ArrowLeft) and must be preserved.\nconst CODE_TOKEN_RE = /(^|\\+)code:([A-Za-z][A-Za-z0-9_]*)/\n\nfunction parseHotkey(hotkeyStr) {\n const raw = String(hotkeyStr || '').trim()\n\n // Detect ' up' suffix case-insensitively so 'Alt UP' works the same as 'alt up'.\n const isUp = /\\s+up$/i.test(raw)\n const baseRaw = isUp ? raw.replace(/\\s+up$/i, '').trim() : raw\n\n // --- AHK-style prefix modifiers ---\n // Examples:\n // ^k => ctrl+k\n // !k => alt+k\n // +k => shift+k\n // #k => meta+k\n // ^!k => ctrl+alt+k\n // !code:KeyX => alt+code:KeyX\n //\n // We strip these BEFORE both the code-token extraction AND the lowercase\n // pass, because the AHK prefix chars (^ ! + #) are case-stable, and the\n // remainder may contain a case-sensitive `code:KeyX` token that must survive.\n const combo = {\n ctrl: false, shift: false, alt: false, meta: false,\n key: null, code: null, bareModifier: null,\n }\n\n let stripped = baseRaw\n while (stripped.length) {\n const mod = ahkPrefixMap.get(stripped[0]) ?? null\n if (!mod) break\n combo[mod] = true\n stripped = stripped.slice(1).trimStart()\n }\n\n // Pull out a code:XXX token before lowercasing; preserve the camelCase value.\n // Reject multiple code: tokens to avoid silent loss of intent.\n const codeMatches = stripped.match(/code:[A-Za-z][A-Za-z0-9_]*/g)\n if (codeMatches && codeMatches.length > 1) {\n throw new Error(\n `Invalid hotkey \"${hotkeyStr}\": multiple code: tokens not supported (got ${codeMatches.length}).`\n )\n }\n let codeValue = null\n const scrubbed = stripped.replace(CODE_TOKEN_RE, (_, sep, val) => {\n codeValue = val\n return sep + '__code__'\n })\n\n let cleanStr = scrubbed.toLowerCase()\n\n // Allow plus key:\n // - \"ctrl++\" => ctrl + \"+\"\n // - \"++\" => shift + \"+\" (AHK '+' prefix sets shift, remaining '+' becomes the base key)\n // `split('+')` produces empties; we keep that signal.\n const rawParts = cleanStr.split('+').map((p) => p.trim())\n const parts = rawParts.filter(Boolean)\n const impliedPlusKey = rawParts.some((p) => p === '') && cleanStr.includes('++')\n\n // Enforce: modifiers must come before the base key.\n // Example: \"ctrl+k\" \u2705 | \"k+ctrl\" \u274C\n // We ignore the special \"+ key\" case (ctrl++) since it has no explicit base token.\n if (!impliedPlusKey) {\n let seenBase = false\n for (const part of parts) {\n if (isModifierToken(part)) {\n if (seenBase) {\n throw new Error(\n `Invalid hotkey \"${hotkeyStr}\": modifiers must come before the base key (e.g. \"ctrl+k\", not \"k+ctrl\")`\n )\n }\n } else {\n if (!seenBase) seenBase = true\n }\n }\n }\n\n // Continue parsing the remaining tokens (words/symbols/aliases)\n for (const part of parts) {\n if (part === '__code__') {\n combo.code = codeValue\n continue\n }\n const actual = keyAliases[part] ?? part\n if (actual === 'ctrl') combo.ctrl = true\n else if (actual === 'shift') combo.shift = true\n else if (actual === 'alt') combo.alt = true\n else if (actual === 'meta') combo.meta = true\n else combo.key = normalizeKey(actual)\n }\n\n if (!combo.key && !combo.code && impliedPlusKey) combo.key = '+'\n\n // If no base key/code is set, this might be a bare-modifier binding ('alt' / 'ctrl').\n // Allow exactly ONE modifier with no other tokens. Multi-modifier bare bindings\n // (e.g. 'ctrl+alt') are intentionally rejected \u2014 add a base key for those.\n if (!combo.key && !combo.code) {\n const modCount =\n (combo.ctrl ? 1 : 0) + (combo.shift ? 1 : 0) +\n (combo.alt ? 1 : 0) + (combo.meta ? 1 : 0)\n\n if (modCount === 1) {\n combo.bareModifier =\n combo.ctrl ? 'ctrl' :\n combo.shift ? 'shift' :\n combo.alt ? 'alt' : 'meta'\n } else if (modCount > 1) {\n throw new Error(\n `Invalid hotkey \"${hotkeyStr}\": multi-modifier bare bindings not supported. Add a base key (e.g. \"ctrl+alt+x\").`\n )\n } else {\n throw new Error(`Invalid hotkey \"${hotkeyStr}\" (missing base key)`)\n }\n }\n\n return { combo, type: isUp ? 'keyup' : 'keydown', raw: hotkeyStr }\n}\n\n\nfunction isFromInputTarget(e) {\n const t = e?.target\n if (!t) return false\n const tag = String(t.tagName || '').toUpperCase()\n return (\n t.isContentEditable ||\n tag === 'INPUT' ||\n tag === 'TEXTAREA' ||\n tag === 'SELECT' ||\n t.getAttribute?.('role') === 'textbox'\n )\n}\n\n\n// --- binding model ---\n// options:\n// preventDefault, stopPropagation, stopImmediatePropagation\n// repeat (default false on keydown), once\n// when(e) -> boolean (gate)\n// allowIn(e) -> boolean (override ignoreInput)\n// priority (higher wins)\nfunction normalizeOptions(type, options) {\n const o = options ? { ...options } : {}\n if (type === 'keydown' && o.repeat == null) o.repeat = false\n if (o.priority == null) o.priority = 0\n return o\n}\n\n\nfunction ensureSlot(comboStr, type) {\n let typeMap = registry.get(comboStr)\n if (!typeMap) {\n typeMap = new Map()\n registry.set(comboStr, typeMap)\n }\n let arr = typeMap.get(type)\n if (!arr) {\n arr = []\n typeMap.set(type, arr)\n }\n return arr\n}\n\n\nfunction removeById(id) {\n const idx = bindingIndex.get(id)\n if (!idx) return false\n\n const { comboStr, type } = idx\n const typeMap = registry.get(comboStr)\n const arr = typeMap?.get(type)\n if (!arr?.length) {\n bindingIndex.delete(id)\n return false\n }\n\n const next = arr.filter((b) => b.id !== id)\n if (next.length) typeMap.set(type, next)\n else typeMap.delete(type)\n\n if (!typeMap.size) registry.delete(comboStr)\n bindingIndex.delete(id)\n return true\n}\n\n\n// --- public API: bind / unbind ---\n// bind returns an off() function; off.id is exposed for plugin bookkeeping.\nfunction bind(hotkeyStr, handler, plugin = null, options = {}) {\n const { combo, type, raw } = parseHotkey(hotkeyStr)\n const comboStr = comboKeyFromParts(combo)\n const opts = normalizeOptions(type, options)\n\n const binding = {\n id: nextId++,\n comboStr,\n type,\n combo,\n handler,\n plugin,\n raw,\n options: opts,\n }\n\n ensureSlot(comboStr, type).push(binding)\n bindingIndex.set(binding.id, { comboStr, type })\n\n // Notify bind subscribers. Hook errors must never break the bind itself.\n if (bindHooks.length) {\n for (const hook of bindHooks) {\n try {\n hook({ combo, raw, options: opts, plugin, id: binding.id })\n } catch (err) {\n // eslint-disable-next-line no-console\n console.error('[hotkey-router] onBind hook threw:', err)\n }\n }\n }\n\n const off = () => removeById(binding.id)\n off.id = binding.id\n off.hotkey = raw\n return off\n}\n\n// Subscribe to bind events. The hook receives\n// { combo, raw, options, plugin, id }\n// after the binding is registered. Returns an unsubscribe function. Used by\n// extensions like `installReservationWarnings` from `hotkey-router/reservations`.\nfunction onBind(hook) {\n if (typeof hook !== 'function') {\n throw new TypeError('hotkeys.onBind(hook) expects a function')\n }\n bindHooks.push(hook)\n return () => {\n bindHooks = bindHooks.filter((h) => h !== hook)\n }\n}\n\n\n// unbind('ctrl+k') -> remove all for that combo/type\n// unbind('ctrl+k', fn) -> remove only that handler\nfunction unbind(hotkeyStr, handler) {\n const { combo, type } = parseHotkey(hotkeyStr)\n const comboStr = comboKeyFromParts(combo)\n const typeMap = registry.get(comboStr)\n if (!typeMap) return\n\n const arr = typeMap.get(type)\n if (!arr?.length) return\n\n if (!handler) {\n for (const b of arr) bindingIndex.delete(b.id)\n typeMap.delete(type)\n } else {\n const keep = []\n for (const b of arr) {\n if (b.handler === handler) bindingIndex.delete(b.id)\n else keep.push(b)\n }\n if (keep.length) typeMap.set(type, keep)\n else typeMap.delete(type)\n }\n\n if (!typeMap.size) registry.delete(comboStr)\n}\n\n\n// --- plugin system ---\n// registerPlugin returns unregister function.\nfunction registerPlugin(name, hotkeyMap) {\n if (plugins.has(name)) throw new Error(`Plugin \"${name}\" already registered`)\n const ids = []\n\n for (const [hotkeyStr, fnOrTuple] of Object.entries(hotkeyMap)) {\n const [fn, options] = Array.isArray(fnOrTuple) ? fnOrTuple : [fnOrTuple, {}]\n const off = bind(hotkeyStr, fn, name, options)\n ids.push(off.id)\n }\n\n plugins.set(name, ids)\n return () => unregisterPlugin(name)\n}\n\nfunction unregisterPlugin(name) {\n const ids = plugins.get(name)\n if (!ids) return\n for (const id of ids) removeById(id)\n plugins.delete(name)\n}\n\n// --- control ---\nfunction pause() {\n paused = true\n}\n\nfunction resume() {\n paused = false\n}\n\nfunction ignoreInput(value = true) {\n ignoreEditable = !!value\n}\n\n// allow changing the default target (iframes/tests).\n// we intentionally do not auto-rebind to keep the core tiny.\nfunction setTarget(target) {\n defaultTarget = target\n}\n\n\n// --- dispatch (O(1) lookup) ---\nfunction pickWinner(bindings) {\n // Highest priority wins; ties -> newest wins (largest id).\n let best = null\n for (const b of bindings) {\n if (!best) best = b\n else if (b.options.priority > best.options.priority) best = b\n else if (b.options.priority === best.options.priority && b.id > best.id) best = b\n }\n return best\n}\n\n\nfunction handleEvent(type) {\n return (e) => {\n if (paused) return\n\n const fromInput = ignoreEditable && isFromInputTarget(e)\n const comboStr = comboKeyFromEvent(e)\n const codeStr = codeKeyFromEvent(e)\n\n // Look up under both lookup keys: key-based (`alt+x`) and code-based\n // (`alt+code:KeyX`). Same physical keypress, two valid binding shapes.\n // Merge candidates and let the existing priority/recency arbiter pick.\n const seen = new Set()\n const candidates = []\n for (const lookupKey of [comboStr, codeStr]) {\n if (!lookupKey || seen.has(lookupKey)) continue\n seen.add(lookupKey)\n\n const bindings = registry.get(lookupKey)?.get(type)\n if (!bindings?.length) continue\n\n for (const b of bindings) {\n const o = b.options\n if (fromInput && !o.allowIn?.(e)) continue\n if (type === 'keydown' && o.repeat === false && e.repeat) continue\n if (o.when && o.when(e) === false) continue\n candidates.push(b)\n }\n }\n if (!candidates.length) return\n\n const winner = pickWinner(candidates)\n const o = winner.options\n\n if (o.preventDefault) e.preventDefault()\n if (o.stopImmediatePropagation) e.stopImmediatePropagation()\n else if (o.stopPropagation) e.stopPropagation()\n\n winner.handler(e)\n\n if (o.once) removeById(winner.id)\n }\n}\n\n\n// --- listeners ---\nlet stopDown = null\nlet stopUp = null\n\nfunction init({ target = defaultTarget, capture = false } = {}) {\n if (!target) throw new Error('hotkeys.init() requires a target (e.g. window)')\n if (stopDown || stopUp) destroy()\n\n stopDown = on(target, 'keydown', handleEvent('keydown'), { capture })\n stopUp = on(target, 'keyup', handleEvent('keyup'), { capture })\n}\n\n\nfunction destroy() {\n stopDown?.()\n stopUp?.()\n stopDown = null\n stopUp = null\n registry.clear()\n plugins.clear()\n bindingIndex.clear()\n // Note: bindHooks deliberately survive destroy(). Consumer extensions\n // (reservations, future telemetry, etc.) install once at startup and\n // shouldn't have to re-attach when the router restarts. Tests that need\n // a clean slate should use the uninstall fn returned by their installer.\n}\n\n\n// --- programmatic trigger (tests / automation) ---\n//\n// Reverse map: 'alt' -> 'Alt' (the KeyboardEvent.key value for that bare modifier).\n// Used to populate the synthetic event's `.key` field so consumer handlers that\n// inspect `e.key` see the same value they'd get from a real event.\nconst BARE_MOD_TO_KEY = {\n ctrl: 'Control',\n shift: 'Shift',\n alt: 'Alt',\n meta: 'Meta',\n}\n\nfunction trigger(hotkeyStr, { type: forcedType } = {}) {\n const { combo, type } = parseHotkey(hotkeyStr)\n const comboStr = comboKeyFromParts(combo)\n const actualType = forcedType || type\n\n const bindings = registry.get(comboStr)?.get(actualType)\n if (!bindings?.length) return false\n\n // Fill `.key` from bareModifier when the binding is bare-modifier-only;\n // otherwise use combo.key. `.code` is set from combo.code for code-based bindings.\n const eventKey = combo.bareModifier\n ? BARE_MOD_TO_KEY[combo.bareModifier]\n : combo.key\n\n // For bare-modifier keyup, real KeyboardEvents have the corresponding\n // modifier flag FALSE (the modifier just released). Mirror that so\n // automation handlers that inspect e.altKey / e.ctrlKey / etc. see the\n // same value they'd see from a real event.\n let altKey = !!combo.alt\n let ctrlKey = !!combo.ctrl\n let shiftKey = !!combo.shift\n let metaKey = !!combo.meta\n if (combo.bareModifier && actualType === 'keyup') {\n if (combo.bareModifier === 'alt') altKey = false\n else if (combo.bareModifier === 'ctrl') ctrlKey = false\n else if (combo.bareModifier === 'shift') shiftKey = false\n else if (combo.bareModifier === 'meta') metaKey = false\n }\n\n const e = {\n type: actualType,\n key: eventKey,\n code: combo.code || null,\n ctrlKey, shiftKey, altKey, metaKey,\n repeat: false,\n preventDefault() { },\n stopPropagation() { },\n stopImmediatePropagation() { },\n target: null,\n }\n\n const winner = pickWinner(\n bindings.filter((b) => (b.options.when ? b.options.when(e) !== false : true))\n )\n if (!winner) return false\n\n winner.handler(e)\n if (winner.options.once) removeById(winner.id)\n return true\n}\n\n\n// Auto-init on window by default (browser only)\nif (defaultTarget) init()\n\nexport default {\n // lifecycle\n init,\n destroy,\n setTarget,\n\n // bindings\n bind,\n unbind,\n registerPlugin,\n unregisterPlugin,\n\n // extension hooks\n onBind,\n\n // control\n pause,\n resume,\n ignoreInput,\n\n // testing / automation\n trigger,\n}"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;
|
|
4
|
+
"sourcesContent": ["// hotkey-router.js\n// Hotkey Router \u2014 plugin-first keyboard shortcut router (tiny + predictable)\n//\n// Goals:\n// - O(1) dispatch by combo string lookup\n// - Deterministic routing: highest priority wins; ties -> most recently bound wins\n// - Safe plugin cleanup (never deletes other plugins\u2019 bindings)\n// - Modern browser target (KeyboardEvent.key + addEventListener)\n//\n// This core ships ZERO reservation data \u2014 opt in via `hotkey-router/auto`\n// or by calling `installReservationWarnings(hotkeys)` from the separate\n// `hotkey-router/reservations` module. Bundlers tree-shake the data file\n// out entirely when neither is imported.\n\n// -----------------------------------------------------------------------------\n// Public type surface (consumed by the tsc-from-JSDoc pipeline)\n// -----------------------------------------------------------------------------\n\n/**\n * Handler invoked when a bound hotkey fires. Receives the underlying\n * `KeyboardEvent` (real one from the DOM, or a synthetic one produced by\n * {@link trigger}).\n *\n * @typedef {(e: KeyboardEvent) => void} HotkeyHandler\n */\n\n/**\n * Optional per-binding behavior modifiers. See the inline comment on\n * `normalizeOptions` for the canonical list.\n *\n * @typedef {{\n * preventDefault?: boolean,\n * stopPropagation?: boolean,\n * stopImmediatePropagation?: boolean,\n * repeat?: boolean,\n * once?: boolean,\n * when?: (e: KeyboardEvent) => boolean,\n * allowIn?: (e: KeyboardEvent) => boolean,\n * priority?: number\n * }} BindOptions\n */\n\n/**\n * Return value of {@link bind}: callable to unbind, plus `id` (the unique\n * binding id used internally) and `hotkey` (the original input string).\n *\n * @typedef {(() => boolean) & { id: number, hotkey: string }} BindOff\n */\n\n/**\n * Payload passed to subscribers registered via {@link onBind} after each\n * successful bind.\n *\n * @typedef {{\n * combo: ReturnType<typeof parseHotkey>['combo'],\n * raw: string,\n * options: BindOptions,\n * plugin: string | null,\n * id: number\n * }} BindEvent\n */\n\n/**\n * Plugin shape accepted by {@link registerPlugin}: a map from hotkey\n * strings to either a handler or a `[handler, options]` tuple.\n *\n * @typedef {Record<string, HotkeyHandler | [HotkeyHandler, BindOptions]>} HotkeyMap\n */\n\n// --- tiny event helper ---\nfunction on(target, type, fn, options) {\n target.addEventListener(type, fn, options)\n return () => target.removeEventListener(type, fn, options)\n}\n\n// --- key alias map ---\nconst isMac =\n typeof navigator !== 'undefined' && /Mac|iPhone|iPad|iPod/.test(navigator.platform)\n\n\nconst modifierTokens = new Set(['ctrl', 'shift', 'alt', 'meta'])\n\n// Map from KeyboardEvent.key (which IS the modifier name on bare-modifier events)\n// to the canonical token used in combo strings. Used to detect bare-modifier\n// keydown/keyup events at dispatch time.\nconst MODIFIER_KEY_TOKEN = Object.freeze({\n Alt: 'alt',\n AltGraph: 'alt',\n Control: 'ctrl',\n Shift: 'shift',\n Meta: 'meta',\n OS: 'meta',\n})\n\nconst ahkPrefixMap = new Map([\n ['^', 'ctrl'], // Ctrl\n ['!', 'alt'], // Alt\n ['+', 'shift'], // Shift\n ['#', 'meta'], // Win / Command\n])\n\n\nconst keyAliases = Object.freeze({\n // common\n esc: 'escape',\n escape: 'escape',\n space: ' ',\n spacebar: ' ',\n enter: 'enter',\n return: 'enter',\n del: 'delete',\n delete: 'delete',\n backspace: 'backspace',\n tab: 'tab',\n\n // arrows\n up: 'arrowup',\n down: 'arrowdown',\n left: 'arrowleft',\n right: 'arrowright',\n\n // navigation\n home: 'home',\n end: 'end',\n pageup: 'pageup',\n pagedown: 'pagedown',\n pgup: 'pageup',\n pgdn: 'pagedown',\n\n // modifiers (words)\n cmd: 'meta',\n command: 'meta',\n win: 'meta',\n meta: 'meta',\n control: 'ctrl',\n ctrl: 'ctrl',\n option: 'alt',\n alt: 'alt',\n shift: 'shift',\n\n // modifiers (symbols)\n '\u2318': 'meta',\n '\u2303': 'ctrl',\n '\u2325': 'alt',\n '\u21E7': 'shift',\n\n // cross-platform \"mod\"\n mod: isMac ? 'meta' : 'ctrl',\n\n // plus key ergonomics\n plus: '+',\n})\n\n// --- config ---\nlet ignoreEditable = true\nlet defaultTarget = typeof window !== 'undefined' ? window : null\n\n// Bind hooks: subscribers called after each successful bind. Used by the\n// optional reservations module to emit conflict warnings without coupling\n// the core router to the (~5 KB) reservation table.\nlet bindHooks = []\n\n// --- state ---\n// comboStr -> Map<type, Array<Binding>>\nconst registry = new Map()\n// pluginName -> Array<bindingId>\nconst plugins = new Map()\n// bindingId -> { comboStr, type }\nconst bindingIndex = new Map()\n\nlet paused = false\nlet nextId = 1\n\n// --- helpers ---\nfunction normalizeKey(key) {\n const original = String(key ?? '')\n // A lone space IS the spacebar key \u2014 `.trim()` would erase it to '', so guard\n // on the ORIGINAL before trimming. (The `space`/`spacebar` aliases resolve to\n // ' ' and land here; without this, `space` bindings collapse to no base key.)\n if (original === ' ') return ' '\n const trimmed = original.trim()\n const k = trimmed.toLowerCase()\n return keyAliases[k] ?? k\n}\n\nfunction comboKeyFromParts({ ctrl, shift, alt, meta, key, code, bareModifier }) {\n // Bare-modifier bindings get a sentinel namespace so they can't collide with\n // regular chord combo strings (`'alt+x'` vs `'__bare__:alt'`).\n if (bareModifier) return `__bare__:${bareModifier}`\n\n let s = ''\n if (ctrl) s += 'ctrl+'\n if (shift) s += 'shift+'\n if (alt) s += 'alt+'\n if (meta) s += 'meta+'\n\n // Code-based combos use a `code:KeyX` suffix instead of the key character.\n if (code) return s + 'code:' + code\n return s ? s + key : key\n}\n\nfunction comboKeyFromEvent(e) {\n // If the event's `key` is itself a modifier name, this is a bare-modifier\n // press/release. Match it via the bare-modifier sentinel regardless of which\n // other modifier flags are set (loose match \u2014 Alt fires `alt` even when held\n // alongside Ctrl).\n const bareMod = MODIFIER_KEY_TOKEN[e.key]\n if (bareMod) return `__bare__:${bareMod}`\n\n return comboKeyFromParts({\n ctrl: e.ctrlKey,\n shift: e.shiftKey,\n alt: e.altKey,\n meta: e.metaKey,\n key: normalizeKey(e.key),\n })\n}\n\n// Code-based lookup string for an event. Independent of `comboKeyFromEvent`\n// because consumers can register either key-based OR code-based bindings (or\n// both) for the same physical key combo, and we look up under whichever they\n// chose.\nfunction codeKeyFromEvent(e) {\n if (!e.code) return null\n\n let s = ''\n if (e.ctrlKey) s += 'ctrl+'\n if (e.shiftKey) s += 'shift+'\n if (e.altKey) s += 'alt+'\n if (e.metaKey) s += 'meta+'\n return s + 'code:' + e.code\n}\n\n\nfunction isModifierToken(token) {\n const t = normalizeKey(token)\n return modifierTokens.has(t)\n}\n\n\n// Matches `code:KeyX` (case-sensitive value). Used to extract code tokens BEFORE\n// the rest of the parser lowercases everything, since KeyboardEvent.code values\n// are camelCase (KeyA, Digit1, ArrowLeft) and must be preserved.\nconst CODE_TOKEN_RE = /(^|\\+)code:([A-Za-z][A-Za-z0-9_]*)/\n\nfunction parseHotkey(hotkeyStr) {\n const raw = String(hotkeyStr || '').trim()\n\n // Detect ' up' suffix case-insensitively so 'Alt UP' works the same as 'alt up'.\n const isUp = /\\s+up$/i.test(raw)\n const baseRaw = isUp ? raw.replace(/\\s+up$/i, '').trim() : raw\n\n // --- AHK-style prefix modifiers ---\n // Examples:\n // ^k => ctrl+k\n // !k => alt+k\n // +k => shift+k\n // #k => meta+k\n // ^!k => ctrl+alt+k\n // !code:KeyX => alt+code:KeyX\n //\n // We strip these BEFORE both the code-token extraction AND the lowercase\n // pass, because the AHK prefix chars (^ ! + #) are case-stable, and the\n // remainder may contain a case-sensitive `code:KeyX` token that must survive.\n const combo = {\n ctrl: false, shift: false, alt: false, meta: false,\n key: null, code: null, bareModifier: null,\n }\n\n let stripped = baseRaw\n while (stripped.length) {\n const mod = ahkPrefixMap.get(stripped[0]) ?? null\n if (!mod) break\n combo[mod] = true\n stripped = stripped.slice(1).trimStart()\n }\n\n // Pull out a code:XXX token before lowercasing; preserve the camelCase value.\n // Reject multiple code: tokens to avoid silent loss of intent.\n const codeMatches = stripped.match(/code:[A-Za-z][A-Za-z0-9_]*/g)\n if (codeMatches && codeMatches.length > 1) {\n throw new Error(\n `Invalid hotkey \"${hotkeyStr}\": multiple code: tokens not supported (got ${codeMatches.length}).`\n )\n }\n let codeValue = null\n const scrubbed = stripped.replace(CODE_TOKEN_RE, (_, sep, val) => {\n codeValue = val\n return sep + '__code__'\n })\n\n let cleanStr = scrubbed.toLowerCase()\n\n // Allow plus key:\n // - \"ctrl++\" => ctrl + \"+\"\n // - \"++\" => shift + \"+\" (AHK '+' prefix sets shift, remaining '+' becomes the base key)\n // `split('+')` produces empties; we keep that signal.\n const rawParts = cleanStr.split('+').map((p) => p.trim())\n const parts = rawParts.filter(Boolean)\n const impliedPlusKey = rawParts.some((p) => p === '') && cleanStr.includes('++')\n\n // Enforce: modifiers must come before the base key.\n // Example: \"ctrl+k\" \u2705 | \"k+ctrl\" \u274C\n // We ignore the special \"+ key\" case (ctrl++) since it has no explicit base token.\n if (!impliedPlusKey) {\n let seenBase = false\n for (const part of parts) {\n if (isModifierToken(part)) {\n if (seenBase) {\n throw new Error(\n `Invalid hotkey \"${hotkeyStr}\": modifiers must come before the base key (e.g. \"ctrl+k\", not \"k+ctrl\")`\n )\n }\n } else {\n if (!seenBase) seenBase = true\n }\n }\n }\n\n // Continue parsing the remaining tokens (words/symbols/aliases)\n for (const part of parts) {\n if (part === '__code__') {\n combo.code = codeValue\n continue\n }\n const actual = keyAliases[part] ?? part\n if (actual === 'ctrl') combo.ctrl = true\n else if (actual === 'shift') combo.shift = true\n else if (actual === 'alt') combo.alt = true\n else if (actual === 'meta') combo.meta = true\n else combo.key = normalizeKey(actual)\n }\n\n if (!combo.key && !combo.code && impliedPlusKey) combo.key = '+'\n\n // If no base key/code is set, this might be a bare-modifier binding ('alt' / 'ctrl').\n // Allow exactly ONE modifier with no other tokens. Multi-modifier bare bindings\n // (e.g. 'ctrl+alt') are intentionally rejected \u2014 add a base key for those.\n if (!combo.key && !combo.code) {\n const modCount =\n (combo.ctrl ? 1 : 0) + (combo.shift ? 1 : 0) +\n (combo.alt ? 1 : 0) + (combo.meta ? 1 : 0)\n\n if (modCount === 1) {\n combo.bareModifier =\n combo.ctrl ? 'ctrl' :\n combo.shift ? 'shift' :\n combo.alt ? 'alt' : 'meta'\n } else if (modCount > 1) {\n throw new Error(\n `Invalid hotkey \"${hotkeyStr}\": multi-modifier bare bindings not supported. Add a base key (e.g. \"ctrl+alt+x\").`\n )\n } else {\n throw new Error(`Invalid hotkey \"${hotkeyStr}\" (missing base key)`)\n }\n }\n\n return { combo, type: isUp ? 'keyup' : 'keydown', raw: hotkeyStr }\n}\n\n\nfunction isFromInputTarget(e) {\n const t = e?.target\n if (!t) return false\n const tag = String(t.tagName || '').toUpperCase()\n return (\n t.isContentEditable ||\n tag === 'INPUT' ||\n tag === 'TEXTAREA' ||\n tag === 'SELECT' ||\n t.getAttribute?.('role') === 'textbox'\n )\n}\n\n\n// --- binding model ---\n// options:\n// preventDefault, stopPropagation, stopImmediatePropagation\n// repeat (default false on keydown), once\n// when(e) -> boolean (gate)\n// allowIn(e) -> boolean (override ignoreInput)\n// priority (higher wins)\nfunction normalizeOptions(type, options) {\n const o = options ? { ...options } : {}\n if (type === 'keydown' && o.repeat == null) o.repeat = false\n if (o.priority == null) o.priority = 0\n return o\n}\n\n\nfunction ensureSlot(comboStr, type) {\n let typeMap = registry.get(comboStr)\n if (!typeMap) {\n typeMap = new Map()\n registry.set(comboStr, typeMap)\n }\n let arr = typeMap.get(type)\n if (!arr) {\n arr = []\n typeMap.set(type, arr)\n }\n return arr\n}\n\n\nfunction removeById(id) {\n const idx = bindingIndex.get(id)\n if (!idx) return false\n\n const { comboStr, type } = idx\n const typeMap = registry.get(comboStr)\n const arr = typeMap?.get(type)\n if (!arr?.length) {\n bindingIndex.delete(id)\n return false\n }\n\n const next = arr.filter((b) => b.id !== id)\n if (next.length) typeMap.set(type, next)\n else typeMap.delete(type)\n\n if (!typeMap.size) registry.delete(comboStr)\n bindingIndex.delete(id)\n return true\n}\n\n\n// --- public API: bind / unbind ---\n// bind returns an off() function; off.id is exposed for plugin bookkeeping.\n\n/**\n * Bind a hotkey combo string to a handler.\n *\n * @param {string} hotkeyStr e.g. `'ctrl+shift+k'`, `'meta+slash'`, `'alt+code:KeyX'`\n * @param {HotkeyHandler} handler\n * @param {string | null} [plugin] optional plugin/namespace tag for bulk cleanup\n * @param {BindOptions} [options]\n * @returns {BindOff} unbind function with `id` + `hotkey` props\n */\nfunction bind(hotkeyStr, handler, plugin = null, options = {}) {\n const { combo, type, raw } = parseHotkey(hotkeyStr)\n const comboStr = comboKeyFromParts(combo)\n const opts = normalizeOptions(type, options)\n\n const binding = {\n id: nextId++,\n comboStr,\n type,\n combo,\n handler,\n plugin,\n raw,\n options: opts,\n }\n\n ensureSlot(comboStr, type).push(binding)\n bindingIndex.set(binding.id, { comboStr, type })\n\n // Notify bind subscribers. Hook errors must never break the bind itself.\n if (bindHooks.length) {\n for (const hook of bindHooks) {\n try {\n hook({ combo, raw, options: opts, plugin, id: binding.id })\n } catch (err) {\n // eslint-disable-next-line no-console\n console.error('[hotkey-router] onBind hook threw:', err)\n }\n }\n }\n\n const off = () => removeById(binding.id)\n off.id = binding.id\n off.hotkey = raw\n return off\n}\n\n// Subscribe to bind events. The hook receives\n// { combo, raw, options, plugin, id }\n// after the binding is registered. Returns an unsubscribe function. Used by\n// extensions like `installReservationWarnings` from `hotkey-router/reservations`.\n\n/**\n * Subscribe to {@link bind} events. The hook is called after each successful\n * bind with a {@link BindEvent} payload.\n *\n * @param {(info: BindEvent) => void} hook\n * @returns {() => void} unsubscribe\n */\nfunction onBind(hook) {\n if (typeof hook !== 'function') {\n throw new TypeError('hotkeys.onBind(hook) expects a function')\n }\n bindHooks.push(hook)\n return () => {\n bindHooks = bindHooks.filter((h) => h !== hook)\n }\n}\n\n\n/**\n * Remove bindings for a combo string. Omit `handler` to remove all bindings\n * for the combo/type; pass a specific handler to remove just that one.\n *\n * @param {string} hotkeyStr\n * @param {HotkeyHandler} [handler]\n * @returns {void}\n */\nfunction unbind(hotkeyStr, handler) {\n const { combo, type } = parseHotkey(hotkeyStr)\n const comboStr = comboKeyFromParts(combo)\n const typeMap = registry.get(comboStr)\n if (!typeMap) return\n\n const arr = typeMap.get(type)\n if (!arr?.length) return\n\n if (!handler) {\n for (const b of arr) bindingIndex.delete(b.id)\n typeMap.delete(type)\n } else {\n const keep = []\n for (const b of arr) {\n if (b.handler === handler) bindingIndex.delete(b.id)\n else keep.push(b)\n }\n if (keep.length) typeMap.set(type, keep)\n else typeMap.delete(type)\n }\n\n if (!typeMap.size) registry.delete(comboStr)\n}\n\n\n// --- plugin system ---\n// registerPlugin returns unregister function.\n\n/**\n * Register a named plugin that bulk-binds a {@link HotkeyMap}. All bindings\n * are tagged with the plugin name so {@link unregisterPlugin} can clean them\n * up together without touching other plugins' bindings.\n *\n * @param {string} name plugin name (must be unique)\n * @param {HotkeyMap} hotkeyMap\n * @returns {() => void} unregister fn for this plugin\n */\nfunction registerPlugin(name, hotkeyMap) {\n if (plugins.has(name)) throw new Error(`Plugin \"${name}\" already registered`)\n const ids = []\n\n for (const [hotkeyStr, fnOrTuple] of Object.entries(hotkeyMap)) {\n const [fn, options] = Array.isArray(fnOrTuple) ? fnOrTuple : [fnOrTuple, {}]\n const off = bind(hotkeyStr, fn, name, options)\n ids.push(off.id)\n }\n\n plugins.set(name, ids)\n return () => unregisterPlugin(name)\n}\n\n/**\n * Remove all bindings owned by the named plugin. Silent no-op if no such\n * plugin is registered.\n *\n * @param {string} name\n * @returns {void}\n */\nfunction unregisterPlugin(name) {\n const ids = plugins.get(name)\n if (!ids) return\n for (const id of ids) removeById(id)\n plugins.delete(name)\n}\n\n// --- control ---\n\n/** Pause dispatch \u2014 bindings stay registered but no handlers fire. @returns {void} */\nfunction pause() {\n paused = true\n}\n\n/** Resume dispatch after a {@link pause}. @returns {void} */\nfunction resume() {\n paused = false\n}\n\n/**\n * Toggle whether handlers fire when the event target is an editable element\n * (input/textarea/contenteditable). Default `true`. Override per-binding via\n * `options.allowIn`.\n *\n * @param {boolean} [value]\n * @returns {void}\n */\nfunction ignoreInput(value = true) {\n ignoreEditable = !!value\n}\n\n// allow changing the default target (iframes/tests).\n// we intentionally do not auto-rebind to keep the core tiny.\n\n/**\n * Replace the default target (used by {@link init} when no `target` is\n * provided). Useful for iframes and tests. Does NOT auto-rebind existing\n * listeners \u2014 call {@link destroy} + {@link init} to re-attach.\n *\n * @param {EventTarget | null} target\n * @returns {void}\n */\nfunction setTarget(target) {\n defaultTarget = target\n}\n\n\n// --- dispatch (O(1) lookup) ---\nfunction pickWinner(bindings) {\n // Highest priority wins; ties -> newest wins (largest id).\n let best = null\n for (const b of bindings) {\n if (!best) best = b\n else if (b.options.priority > best.options.priority) best = b\n else if (b.options.priority === best.options.priority && b.id > best.id) best = b\n }\n return best\n}\n\n\nfunction handleEvent(type) {\n return (e) => {\n if (paused) return\n\n const fromInput = ignoreEditable && isFromInputTarget(e)\n const comboStr = comboKeyFromEvent(e)\n const codeStr = codeKeyFromEvent(e)\n\n // Look up under both lookup keys: key-based (`alt+x`) and code-based\n // (`alt+code:KeyX`). Same physical keypress, two valid binding shapes.\n // Merge candidates and let the existing priority/recency arbiter pick.\n const seen = new Set()\n const candidates = []\n for (const lookupKey of [comboStr, codeStr]) {\n if (!lookupKey || seen.has(lookupKey)) continue\n seen.add(lookupKey)\n\n const bindings = registry.get(lookupKey)?.get(type)\n if (!bindings?.length) continue\n\n for (const b of bindings) {\n const o = b.options\n if (fromInput && !o.allowIn?.(e)) continue\n if (type === 'keydown' && o.repeat === false && e.repeat) continue\n if (o.when && o.when(e) === false) continue\n candidates.push(b)\n }\n }\n if (!candidates.length) return\n\n const winner = pickWinner(candidates)\n const o = winner.options\n\n if (o.preventDefault) e.preventDefault()\n if (o.stopImmediatePropagation) e.stopImmediatePropagation()\n else if (o.stopPropagation) e.stopPropagation()\n\n winner.handler(e)\n\n if (o.once) removeById(winner.id)\n }\n}\n\n\n// --- listeners ---\nlet stopDown = null\nlet stopUp = null\n\n/**\n * Initialize the router by attaching `keydown`/`keyup` listeners to a target.\n * Re-initializes cleanly if already running (calls {@link destroy} first).\n * Auto-runs on `window` at module-load time in browser environments.\n *\n * @param {{ target?: EventTarget, capture?: boolean }} [opts]\n * @returns {void}\n */\nfunction init({ target = defaultTarget, capture = false } = {}) {\n if (!target) throw new Error('hotkeys.init() requires a target (e.g. window)')\n if (stopDown || stopUp) destroy()\n\n stopDown = on(target, 'keydown', handleEvent('keydown'), { capture })\n stopUp = on(target, 'keyup', handleEvent('keyup'), { capture })\n}\n\n\n/**\n * Tear down listeners and clear all bindings + plugins. Bind-hook\n * subscribers ({@link onBind}) deliberately survive \u2014 consumer\n * extensions should install once and live across `init`/`destroy` cycles.\n *\n * @returns {void}\n */\nfunction destroy() {\n stopDown?.()\n stopUp?.()\n stopDown = null\n stopUp = null\n registry.clear()\n plugins.clear()\n bindingIndex.clear()\n // Note: bindHooks deliberately survive destroy(). Consumer extensions\n // (reservations, future telemetry, etc.) install once at startup and\n // shouldn't have to re-attach when the router restarts. Tests that need\n // a clean slate should use the uninstall fn returned by their installer.\n}\n\n\n// --- programmatic trigger (tests / automation) ---\n//\n// Reverse map: 'alt' -> 'Alt' (the KeyboardEvent.key value for that bare modifier).\n// Used to populate the synthetic event's `.key` field so consumer handlers that\n// inspect `e.key` see the same value they'd get from a real event.\nconst BARE_MOD_TO_KEY = {\n ctrl: 'Control',\n shift: 'Shift',\n alt: 'Alt',\n meta: 'Meta',\n}\n\n/**\n * Programmatically fire any bindings for `hotkeyStr`. Used by tests and\n * automation; not a typical app-runtime path. Returns `true` if a binding\n * was found and invoked, `false` otherwise.\n *\n * @param {string} hotkeyStr\n * @param {{ type?: 'keydown' | 'keyup' }} [opts]\n * @returns {boolean}\n */\nfunction trigger(hotkeyStr, { type: forcedType } = {}) {\n const { combo, type } = parseHotkey(hotkeyStr)\n const comboStr = comboKeyFromParts(combo)\n const actualType = forcedType || type\n\n const bindings = registry.get(comboStr)?.get(actualType)\n if (!bindings?.length) return false\n\n // Fill `.key` from bareModifier when the binding is bare-modifier-only;\n // otherwise use combo.key. `.code` is set from combo.code for code-based bindings.\n const eventKey = combo.bareModifier\n ? BARE_MOD_TO_KEY[combo.bareModifier]\n : combo.key\n\n // For bare-modifier keyup, real KeyboardEvents have the corresponding\n // modifier flag FALSE (the modifier just released). Mirror that so\n // automation handlers that inspect e.altKey / e.ctrlKey / etc. see the\n // same value they'd see from a real event.\n let altKey = !!combo.alt\n let ctrlKey = !!combo.ctrl\n let shiftKey = !!combo.shift\n let metaKey = !!combo.meta\n if (combo.bareModifier && actualType === 'keyup') {\n if (combo.bareModifier === 'alt') altKey = false\n else if (combo.bareModifier === 'ctrl') ctrlKey = false\n else if (combo.bareModifier === 'shift') shiftKey = false\n else if (combo.bareModifier === 'meta') metaKey = false\n }\n\n const e = {\n type: actualType,\n key: eventKey,\n code: combo.code || null,\n ctrlKey, shiftKey, altKey, metaKey,\n repeat: false,\n preventDefault() { },\n stopPropagation() { },\n stopImmediatePropagation() { },\n target: null,\n }\n\n const winner = pickWinner(\n bindings.filter((b) => (b.options.when ? b.options.when(e) !== false : true))\n )\n if (!winner) return false\n\n winner.handler(e)\n if (winner.options.once) removeById(winner.id)\n return true\n}\n\n\n// Auto-init on window by default (browser only)\nif (defaultTarget) init()\n\nexport default {\n // lifecycle\n init,\n destroy,\n setTarget,\n\n // bindings\n bind,\n unbind,\n registerPlugin,\n unregisterPlugin,\n\n // extension hooks\n onBind,\n\n // control\n pause,\n resume,\n ignoreInput,\n\n // testing / automation\n trigger,\n}"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["o"]
|
|
7
7
|
}
|
package/dist/hotkey-router.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// hotkey-router v0.2.
|
|
1
|
+
// hotkey-router v0.2.3 | SEE LICENSE IN LICENSE
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
// hotkey-router.js
|
|
@@ -79,9 +79,11 @@ var bindingIndex = /* @__PURE__ */ new Map();
|
|
|
79
79
|
var paused = false;
|
|
80
80
|
var nextId = 1;
|
|
81
81
|
function normalizeKey(key) {
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
82
|
+
const original = String(key ?? "");
|
|
83
|
+
if (original === " ") return " ";
|
|
84
|
+
const trimmed = original.trim();
|
|
85
|
+
const k = trimmed.toLowerCase();
|
|
86
|
+
return keyAliases[k] ?? k;
|
|
85
87
|
}
|
|
86
88
|
function comboKeyFromParts({ ctrl, shift, alt, meta, key, code, bareModifier }) {
|
|
87
89
|
if (bareModifier) return `__bare__:${bareModifier}`;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../hotkey-router.js"],
|
|
4
|
-
"sourcesContent": ["// hotkey-router.js\n// Hotkey Router \u2014 plugin-first keyboard shortcut router (tiny + predictable)\n//\n// Goals:\n// - O(1) dispatch by combo string lookup\n// - Deterministic routing: highest priority wins; ties -> most recently bound wins\n// - Safe plugin cleanup (never deletes other plugins\u2019 bindings)\n// - Modern browser target (KeyboardEvent.key + addEventListener)\n//\n// This core ships ZERO reservation data \u2014 opt in via `hotkey-router/auto`\n// or by calling `installReservationWarnings(hotkeys)` from the separate\n// `hotkey-router/reservations` module. Bundlers tree-shake the data file\n// out entirely when neither is imported.\n\n// --- tiny event helper ---\nfunction on(target, type, fn, options) {\n target.addEventListener(type, fn, options)\n return () => target.removeEventListener(type, fn, options)\n}\n\n// --- key alias map ---\nconst isMac =\n typeof navigator !== 'undefined' && /Mac|iPhone|iPad|iPod/.test(navigator.platform)\n\n\nconst modifierTokens = new Set(['ctrl', 'shift', 'alt', 'meta'])\n\n// Map from KeyboardEvent.key (which IS the modifier name on bare-modifier events)\n// to the canonical token used in combo strings. Used to detect bare-modifier\n// keydown/keyup events at dispatch time.\nconst MODIFIER_KEY_TOKEN = Object.freeze({\n Alt: 'alt',\n AltGraph: 'alt',\n Control: 'ctrl',\n Shift: 'shift',\n Meta: 'meta',\n OS: 'meta',\n})\n\nconst ahkPrefixMap = new Map([\n ['^', 'ctrl'], // Ctrl\n ['!', 'alt'], // Alt\n ['+', 'shift'], // Shift\n ['#', 'meta'], // Win / Command\n])\n\n\nconst keyAliases = Object.freeze({\n // common\n esc: 'escape',\n escape: 'escape',\n space: ' ',\n spacebar: ' ',\n enter: 'enter',\n return: 'enter',\n del: 'delete',\n delete: 'delete',\n backspace: 'backspace',\n tab: 'tab',\n\n // arrows\n up: 'arrowup',\n down: 'arrowdown',\n left: 'arrowleft',\n right: 'arrowright',\n\n // navigation\n home: 'home',\n end: 'end',\n pageup: 'pageup',\n pagedown: 'pagedown',\n pgup: 'pageup',\n pgdn: 'pagedown',\n\n // modifiers (words)\n cmd: 'meta',\n command: 'meta',\n win: 'meta',\n meta: 'meta',\n control: 'ctrl',\n ctrl: 'ctrl',\n option: 'alt',\n alt: 'alt',\n shift: 'shift',\n\n // modifiers (symbols)\n '\u2318': 'meta',\n '\u2303': 'ctrl',\n '\u2325': 'alt',\n '\u21E7': 'shift',\n\n // cross-platform \"mod\"\n mod: isMac ? 'meta' : 'ctrl',\n\n // plus key ergonomics\n plus: '+',\n})\n\n// --- config ---\nlet ignoreEditable = true\nlet defaultTarget = typeof window !== 'undefined' ? window : null\n\n// Bind hooks: subscribers called after each successful bind. Used by the\n// optional reservations module to emit conflict warnings without coupling\n// the core router to the (~5 KB) reservation table.\nlet bindHooks = []\n\n// --- state ---\n// comboStr -> Map<type, Array<Binding>>\nconst registry = new Map()\n// pluginName -> Array<bindingId>\nconst plugins = new Map()\n// bindingId -> { comboStr, type }\nconst bindingIndex = new Map()\n\nlet paused = false\nlet nextId = 1\n\n// --- helpers ---\nfunction normalizeKey(key) {\n const raw = String(key ?? '').trim()\n const k = raw.toLowerCase()\n return keyAliases[k] ?? (raw === ' ' ? ' ' : k)\n}\n\nfunction comboKeyFromParts({ ctrl, shift, alt, meta, key, code, bareModifier }) {\n // Bare-modifier bindings get a sentinel namespace so they can't collide with\n // regular chord combo strings (`'alt+x'` vs `'__bare__:alt'`).\n if (bareModifier) return `__bare__:${bareModifier}`\n\n let s = ''\n if (ctrl) s += 'ctrl+'\n if (shift) s += 'shift+'\n if (alt) s += 'alt+'\n if (meta) s += 'meta+'\n\n // Code-based combos use a `code:KeyX` suffix instead of the key character.\n if (code) return s + 'code:' + code\n return s ? s + key : key\n}\n\nfunction comboKeyFromEvent(e) {\n // If the event's `key` is itself a modifier name, this is a bare-modifier\n // press/release. Match it via the bare-modifier sentinel regardless of which\n // other modifier flags are set (loose match \u2014 Alt fires `alt` even when held\n // alongside Ctrl).\n const bareMod = MODIFIER_KEY_TOKEN[e.key]\n if (bareMod) return `__bare__:${bareMod}`\n\n return comboKeyFromParts({\n ctrl: e.ctrlKey,\n shift: e.shiftKey,\n alt: e.altKey,\n meta: e.metaKey,\n key: normalizeKey(e.key),\n })\n}\n\n// Code-based lookup string for an event. Independent of `comboKeyFromEvent`\n// because consumers can register either key-based OR code-based bindings (or\n// both) for the same physical key combo, and we look up under whichever they\n// chose.\nfunction codeKeyFromEvent(e) {\n if (!e.code) return null\n\n let s = ''\n if (e.ctrlKey) s += 'ctrl+'\n if (e.shiftKey) s += 'shift+'\n if (e.altKey) s += 'alt+'\n if (e.metaKey) s += 'meta+'\n return s + 'code:' + e.code\n}\n\n\nfunction isModifierToken(token) {\n const t = normalizeKey(token)\n return modifierTokens.has(t)\n}\n\n\n// Matches `code:KeyX` (case-sensitive value). Used to extract code tokens BEFORE\n// the rest of the parser lowercases everything, since KeyboardEvent.code values\n// are camelCase (KeyA, Digit1, ArrowLeft) and must be preserved.\nconst CODE_TOKEN_RE = /(^|\\+)code:([A-Za-z][A-Za-z0-9_]*)/\n\nfunction parseHotkey(hotkeyStr) {\n const raw = String(hotkeyStr || '').trim()\n\n // Detect ' up' suffix case-insensitively so 'Alt UP' works the same as 'alt up'.\n const isUp = /\\s+up$/i.test(raw)\n const baseRaw = isUp ? raw.replace(/\\s+up$/i, '').trim() : raw\n\n // --- AHK-style prefix modifiers ---\n // Examples:\n // ^k => ctrl+k\n // !k => alt+k\n // +k => shift+k\n // #k => meta+k\n // ^!k => ctrl+alt+k\n // !code:KeyX => alt+code:KeyX\n //\n // We strip these BEFORE both the code-token extraction AND the lowercase\n // pass, because the AHK prefix chars (^ ! + #) are case-stable, and the\n // remainder may contain a case-sensitive `code:KeyX` token that must survive.\n const combo = {\n ctrl: false, shift: false, alt: false, meta: false,\n key: null, code: null, bareModifier: null,\n }\n\n let stripped = baseRaw\n while (stripped.length) {\n const mod = ahkPrefixMap.get(stripped[0]) ?? null\n if (!mod) break\n combo[mod] = true\n stripped = stripped.slice(1).trimStart()\n }\n\n // Pull out a code:XXX token before lowercasing; preserve the camelCase value.\n // Reject multiple code: tokens to avoid silent loss of intent.\n const codeMatches = stripped.match(/code:[A-Za-z][A-Za-z0-9_]*/g)\n if (codeMatches && codeMatches.length > 1) {\n throw new Error(\n `Invalid hotkey \"${hotkeyStr}\": multiple code: tokens not supported (got ${codeMatches.length}).`\n )\n }\n let codeValue = null\n const scrubbed = stripped.replace(CODE_TOKEN_RE, (_, sep, val) => {\n codeValue = val\n return sep + '__code__'\n })\n\n let cleanStr = scrubbed.toLowerCase()\n\n // Allow plus key:\n // - \"ctrl++\" => ctrl + \"+\"\n // - \"++\" => shift + \"+\" (AHK '+' prefix sets shift, remaining '+' becomes the base key)\n // `split('+')` produces empties; we keep that signal.\n const rawParts = cleanStr.split('+').map((p) => p.trim())\n const parts = rawParts.filter(Boolean)\n const impliedPlusKey = rawParts.some((p) => p === '') && cleanStr.includes('++')\n\n // Enforce: modifiers must come before the base key.\n // Example: \"ctrl+k\" \u2705 | \"k+ctrl\" \u274C\n // We ignore the special \"+ key\" case (ctrl++) since it has no explicit base token.\n if (!impliedPlusKey) {\n let seenBase = false\n for (const part of parts) {\n if (isModifierToken(part)) {\n if (seenBase) {\n throw new Error(\n `Invalid hotkey \"${hotkeyStr}\": modifiers must come before the base key (e.g. \"ctrl+k\", not \"k+ctrl\")`\n )\n }\n } else {\n if (!seenBase) seenBase = true\n }\n }\n }\n\n // Continue parsing the remaining tokens (words/symbols/aliases)\n for (const part of parts) {\n if (part === '__code__') {\n combo.code = codeValue\n continue\n }\n const actual = keyAliases[part] ?? part\n if (actual === 'ctrl') combo.ctrl = true\n else if (actual === 'shift') combo.shift = true\n else if (actual === 'alt') combo.alt = true\n else if (actual === 'meta') combo.meta = true\n else combo.key = normalizeKey(actual)\n }\n\n if (!combo.key && !combo.code && impliedPlusKey) combo.key = '+'\n\n // If no base key/code is set, this might be a bare-modifier binding ('alt' / 'ctrl').\n // Allow exactly ONE modifier with no other tokens. Multi-modifier bare bindings\n // (e.g. 'ctrl+alt') are intentionally rejected \u2014 add a base key for those.\n if (!combo.key && !combo.code) {\n const modCount =\n (combo.ctrl ? 1 : 0) + (combo.shift ? 1 : 0) +\n (combo.alt ? 1 : 0) + (combo.meta ? 1 : 0)\n\n if (modCount === 1) {\n combo.bareModifier =\n combo.ctrl ? 'ctrl' :\n combo.shift ? 'shift' :\n combo.alt ? 'alt' : 'meta'\n } else if (modCount > 1) {\n throw new Error(\n `Invalid hotkey \"${hotkeyStr}\": multi-modifier bare bindings not supported. Add a base key (e.g. \"ctrl+alt+x\").`\n )\n } else {\n throw new Error(`Invalid hotkey \"${hotkeyStr}\" (missing base key)`)\n }\n }\n\n return { combo, type: isUp ? 'keyup' : 'keydown', raw: hotkeyStr }\n}\n\n\nfunction isFromInputTarget(e) {\n const t = e?.target\n if (!t) return false\n const tag = String(t.tagName || '').toUpperCase()\n return (\n t.isContentEditable ||\n tag === 'INPUT' ||\n tag === 'TEXTAREA' ||\n tag === 'SELECT' ||\n t.getAttribute?.('role') === 'textbox'\n )\n}\n\n\n// --- binding model ---\n// options:\n// preventDefault, stopPropagation, stopImmediatePropagation\n// repeat (default false on keydown), once\n// when(e) -> boolean (gate)\n// allowIn(e) -> boolean (override ignoreInput)\n// priority (higher wins)\nfunction normalizeOptions(type, options) {\n const o = options ? { ...options } : {}\n if (type === 'keydown' && o.repeat == null) o.repeat = false\n if (o.priority == null) o.priority = 0\n return o\n}\n\n\nfunction ensureSlot(comboStr, type) {\n let typeMap = registry.get(comboStr)\n if (!typeMap) {\n typeMap = new Map()\n registry.set(comboStr, typeMap)\n }\n let arr = typeMap.get(type)\n if (!arr) {\n arr = []\n typeMap.set(type, arr)\n }\n return arr\n}\n\n\nfunction removeById(id) {\n const idx = bindingIndex.get(id)\n if (!idx) return false\n\n const { comboStr, type } = idx\n const typeMap = registry.get(comboStr)\n const arr = typeMap?.get(type)\n if (!arr?.length) {\n bindingIndex.delete(id)\n return false\n }\n\n const next = arr.filter((b) => b.id !== id)\n if (next.length) typeMap.set(type, next)\n else typeMap.delete(type)\n\n if (!typeMap.size) registry.delete(comboStr)\n bindingIndex.delete(id)\n return true\n}\n\n\n// --- public API: bind / unbind ---\n// bind returns an off() function; off.id is exposed for plugin bookkeeping.\nfunction bind(hotkeyStr, handler, plugin = null, options = {}) {\n const { combo, type, raw } = parseHotkey(hotkeyStr)\n const comboStr = comboKeyFromParts(combo)\n const opts = normalizeOptions(type, options)\n\n const binding = {\n id: nextId++,\n comboStr,\n type,\n combo,\n handler,\n plugin,\n raw,\n options: opts,\n }\n\n ensureSlot(comboStr, type).push(binding)\n bindingIndex.set(binding.id, { comboStr, type })\n\n // Notify bind subscribers. Hook errors must never break the bind itself.\n if (bindHooks.length) {\n for (const hook of bindHooks) {\n try {\n hook({ combo, raw, options: opts, plugin, id: binding.id })\n } catch (err) {\n // eslint-disable-next-line no-console\n console.error('[hotkey-router] onBind hook threw:', err)\n }\n }\n }\n\n const off = () => removeById(binding.id)\n off.id = binding.id\n off.hotkey = raw\n return off\n}\n\n// Subscribe to bind events. The hook receives\n// { combo, raw, options, plugin, id }\n// after the binding is registered. Returns an unsubscribe function. Used by\n// extensions like `installReservationWarnings` from `hotkey-router/reservations`.\nfunction onBind(hook) {\n if (typeof hook !== 'function') {\n throw new TypeError('hotkeys.onBind(hook) expects a function')\n }\n bindHooks.push(hook)\n return () => {\n bindHooks = bindHooks.filter((h) => h !== hook)\n }\n}\n\n\n// unbind('ctrl+k') -> remove all for that combo/type\n// unbind('ctrl+k', fn) -> remove only that handler\nfunction unbind(hotkeyStr, handler) {\n const { combo, type } = parseHotkey(hotkeyStr)\n const comboStr = comboKeyFromParts(combo)\n const typeMap = registry.get(comboStr)\n if (!typeMap) return\n\n const arr = typeMap.get(type)\n if (!arr?.length) return\n\n if (!handler) {\n for (const b of arr) bindingIndex.delete(b.id)\n typeMap.delete(type)\n } else {\n const keep = []\n for (const b of arr) {\n if (b.handler === handler) bindingIndex.delete(b.id)\n else keep.push(b)\n }\n if (keep.length) typeMap.set(type, keep)\n else typeMap.delete(type)\n }\n\n if (!typeMap.size) registry.delete(comboStr)\n}\n\n\n// --- plugin system ---\n// registerPlugin returns unregister function.\nfunction registerPlugin(name, hotkeyMap) {\n if (plugins.has(name)) throw new Error(`Plugin \"${name}\" already registered`)\n const ids = []\n\n for (const [hotkeyStr, fnOrTuple] of Object.entries(hotkeyMap)) {\n const [fn, options] = Array.isArray(fnOrTuple) ? fnOrTuple : [fnOrTuple, {}]\n const off = bind(hotkeyStr, fn, name, options)\n ids.push(off.id)\n }\n\n plugins.set(name, ids)\n return () => unregisterPlugin(name)\n}\n\nfunction unregisterPlugin(name) {\n const ids = plugins.get(name)\n if (!ids) return\n for (const id of ids) removeById(id)\n plugins.delete(name)\n}\n\n// --- control ---\nfunction pause() {\n paused = true\n}\n\nfunction resume() {\n paused = false\n}\n\nfunction ignoreInput(value = true) {\n ignoreEditable = !!value\n}\n\n// allow changing the default target (iframes/tests).\n// we intentionally do not auto-rebind to keep the core tiny.\nfunction setTarget(target) {\n defaultTarget = target\n}\n\n\n// --- dispatch (O(1) lookup) ---\nfunction pickWinner(bindings) {\n // Highest priority wins; ties -> newest wins (largest id).\n let best = null\n for (const b of bindings) {\n if (!best) best = b\n else if (b.options.priority > best.options.priority) best = b\n else if (b.options.priority === best.options.priority && b.id > best.id) best = b\n }\n return best\n}\n\n\nfunction handleEvent(type) {\n return (e) => {\n if (paused) return\n\n const fromInput = ignoreEditable && isFromInputTarget(e)\n const comboStr = comboKeyFromEvent(e)\n const codeStr = codeKeyFromEvent(e)\n\n // Look up under both lookup keys: key-based (`alt+x`) and code-based\n // (`alt+code:KeyX`). Same physical keypress, two valid binding shapes.\n // Merge candidates and let the existing priority/recency arbiter pick.\n const seen = new Set()\n const candidates = []\n for (const lookupKey of [comboStr, codeStr]) {\n if (!lookupKey || seen.has(lookupKey)) continue\n seen.add(lookupKey)\n\n const bindings = registry.get(lookupKey)?.get(type)\n if (!bindings?.length) continue\n\n for (const b of bindings) {\n const o = b.options\n if (fromInput && !o.allowIn?.(e)) continue\n if (type === 'keydown' && o.repeat === false && e.repeat) continue\n if (o.when && o.when(e) === false) continue\n candidates.push(b)\n }\n }\n if (!candidates.length) return\n\n const winner = pickWinner(candidates)\n const o = winner.options\n\n if (o.preventDefault) e.preventDefault()\n if (o.stopImmediatePropagation) e.stopImmediatePropagation()\n else if (o.stopPropagation) e.stopPropagation()\n\n winner.handler(e)\n\n if (o.once) removeById(winner.id)\n }\n}\n\n\n// --- listeners ---\nlet stopDown = null\nlet stopUp = null\n\nfunction init({ target = defaultTarget, capture = false } = {}) {\n if (!target) throw new Error('hotkeys.init() requires a target (e.g. window)')\n if (stopDown || stopUp) destroy()\n\n stopDown = on(target, 'keydown', handleEvent('keydown'), { capture })\n stopUp = on(target, 'keyup', handleEvent('keyup'), { capture })\n}\n\n\nfunction destroy() {\n stopDown?.()\n stopUp?.()\n stopDown = null\n stopUp = null\n registry.clear()\n plugins.clear()\n bindingIndex.clear()\n // Note: bindHooks deliberately survive destroy(). Consumer extensions\n // (reservations, future telemetry, etc.) install once at startup and\n // shouldn't have to re-attach when the router restarts. Tests that need\n // a clean slate should use the uninstall fn returned by their installer.\n}\n\n\n// --- programmatic trigger (tests / automation) ---\n//\n// Reverse map: 'alt' -> 'Alt' (the KeyboardEvent.key value for that bare modifier).\n// Used to populate the synthetic event's `.key` field so consumer handlers that\n// inspect `e.key` see the same value they'd get from a real event.\nconst BARE_MOD_TO_KEY = {\n ctrl: 'Control',\n shift: 'Shift',\n alt: 'Alt',\n meta: 'Meta',\n}\n\nfunction trigger(hotkeyStr, { type: forcedType } = {}) {\n const { combo, type } = parseHotkey(hotkeyStr)\n const comboStr = comboKeyFromParts(combo)\n const actualType = forcedType || type\n\n const bindings = registry.get(comboStr)?.get(actualType)\n if (!bindings?.length) return false\n\n // Fill `.key` from bareModifier when the binding is bare-modifier-only;\n // otherwise use combo.key. `.code` is set from combo.code for code-based bindings.\n const eventKey = combo.bareModifier\n ? BARE_MOD_TO_KEY[combo.bareModifier]\n : combo.key\n\n // For bare-modifier keyup, real KeyboardEvents have the corresponding\n // modifier flag FALSE (the modifier just released). Mirror that so\n // automation handlers that inspect e.altKey / e.ctrlKey / etc. see the\n // same value they'd see from a real event.\n let altKey = !!combo.alt\n let ctrlKey = !!combo.ctrl\n let shiftKey = !!combo.shift\n let metaKey = !!combo.meta\n if (combo.bareModifier && actualType === 'keyup') {\n if (combo.bareModifier === 'alt') altKey = false\n else if (combo.bareModifier === 'ctrl') ctrlKey = false\n else if (combo.bareModifier === 'shift') shiftKey = false\n else if (combo.bareModifier === 'meta') metaKey = false\n }\n\n const e = {\n type: actualType,\n key: eventKey,\n code: combo.code || null,\n ctrlKey, shiftKey, altKey, metaKey,\n repeat: false,\n preventDefault() { },\n stopPropagation() { },\n stopImmediatePropagation() { },\n target: null,\n }\n\n const winner = pickWinner(\n bindings.filter((b) => (b.options.when ? b.options.when(e) !== false : true))\n )\n if (!winner) return false\n\n winner.handler(e)\n if (winner.options.once) removeById(winner.id)\n return true\n}\n\n\n// Auto-init on window by default (browser only)\nif (defaultTarget) init()\n\nexport default {\n // lifecycle\n init,\n destroy,\n setTarget,\n\n // bindings\n bind,\n unbind,\n registerPlugin,\n unregisterPlugin,\n\n // extension hooks\n onBind,\n\n // control\n pause,\n resume,\n ignoreInput,\n\n // testing / automation\n trigger,\n}"],
|
|
5
|
-
"mappings": ";;;;
|
|
4
|
+
"sourcesContent": ["// hotkey-router.js\n// Hotkey Router \u2014 plugin-first keyboard shortcut router (tiny + predictable)\n//\n// Goals:\n// - O(1) dispatch by combo string lookup\n// - Deterministic routing: highest priority wins; ties -> most recently bound wins\n// - Safe plugin cleanup (never deletes other plugins\u2019 bindings)\n// - Modern browser target (KeyboardEvent.key + addEventListener)\n//\n// This core ships ZERO reservation data \u2014 opt in via `hotkey-router/auto`\n// or by calling `installReservationWarnings(hotkeys)` from the separate\n// `hotkey-router/reservations` module. Bundlers tree-shake the data file\n// out entirely when neither is imported.\n\n// -----------------------------------------------------------------------------\n// Public type surface (consumed by the tsc-from-JSDoc pipeline)\n// -----------------------------------------------------------------------------\n\n/**\n * Handler invoked when a bound hotkey fires. Receives the underlying\n * `KeyboardEvent` (real one from the DOM, or a synthetic one produced by\n * {@link trigger}).\n *\n * @typedef {(e: KeyboardEvent) => void} HotkeyHandler\n */\n\n/**\n * Optional per-binding behavior modifiers. See the inline comment on\n * `normalizeOptions` for the canonical list.\n *\n * @typedef {{\n * preventDefault?: boolean,\n * stopPropagation?: boolean,\n * stopImmediatePropagation?: boolean,\n * repeat?: boolean,\n * once?: boolean,\n * when?: (e: KeyboardEvent) => boolean,\n * allowIn?: (e: KeyboardEvent) => boolean,\n * priority?: number\n * }} BindOptions\n */\n\n/**\n * Return value of {@link bind}: callable to unbind, plus `id` (the unique\n * binding id used internally) and `hotkey` (the original input string).\n *\n * @typedef {(() => boolean) & { id: number, hotkey: string }} BindOff\n */\n\n/**\n * Payload passed to subscribers registered via {@link onBind} after each\n * successful bind.\n *\n * @typedef {{\n * combo: ReturnType<typeof parseHotkey>['combo'],\n * raw: string,\n * options: BindOptions,\n * plugin: string | null,\n * id: number\n * }} BindEvent\n */\n\n/**\n * Plugin shape accepted by {@link registerPlugin}: a map from hotkey\n * strings to either a handler or a `[handler, options]` tuple.\n *\n * @typedef {Record<string, HotkeyHandler | [HotkeyHandler, BindOptions]>} HotkeyMap\n */\n\n// --- tiny event helper ---\nfunction on(target, type, fn, options) {\n target.addEventListener(type, fn, options)\n return () => target.removeEventListener(type, fn, options)\n}\n\n// --- key alias map ---\nconst isMac =\n typeof navigator !== 'undefined' && /Mac|iPhone|iPad|iPod/.test(navigator.platform)\n\n\nconst modifierTokens = new Set(['ctrl', 'shift', 'alt', 'meta'])\n\n// Map from KeyboardEvent.key (which IS the modifier name on bare-modifier events)\n// to the canonical token used in combo strings. Used to detect bare-modifier\n// keydown/keyup events at dispatch time.\nconst MODIFIER_KEY_TOKEN = Object.freeze({\n Alt: 'alt',\n AltGraph: 'alt',\n Control: 'ctrl',\n Shift: 'shift',\n Meta: 'meta',\n OS: 'meta',\n})\n\nconst ahkPrefixMap = new Map([\n ['^', 'ctrl'], // Ctrl\n ['!', 'alt'], // Alt\n ['+', 'shift'], // Shift\n ['#', 'meta'], // Win / Command\n])\n\n\nconst keyAliases = Object.freeze({\n // common\n esc: 'escape',\n escape: 'escape',\n space: ' ',\n spacebar: ' ',\n enter: 'enter',\n return: 'enter',\n del: 'delete',\n delete: 'delete',\n backspace: 'backspace',\n tab: 'tab',\n\n // arrows\n up: 'arrowup',\n down: 'arrowdown',\n left: 'arrowleft',\n right: 'arrowright',\n\n // navigation\n home: 'home',\n end: 'end',\n pageup: 'pageup',\n pagedown: 'pagedown',\n pgup: 'pageup',\n pgdn: 'pagedown',\n\n // modifiers (words)\n cmd: 'meta',\n command: 'meta',\n win: 'meta',\n meta: 'meta',\n control: 'ctrl',\n ctrl: 'ctrl',\n option: 'alt',\n alt: 'alt',\n shift: 'shift',\n\n // modifiers (symbols)\n '\u2318': 'meta',\n '\u2303': 'ctrl',\n '\u2325': 'alt',\n '\u21E7': 'shift',\n\n // cross-platform \"mod\"\n mod: isMac ? 'meta' : 'ctrl',\n\n // plus key ergonomics\n plus: '+',\n})\n\n// --- config ---\nlet ignoreEditable = true\nlet defaultTarget = typeof window !== 'undefined' ? window : null\n\n// Bind hooks: subscribers called after each successful bind. Used by the\n// optional reservations module to emit conflict warnings without coupling\n// the core router to the (~5 KB) reservation table.\nlet bindHooks = []\n\n// --- state ---\n// comboStr -> Map<type, Array<Binding>>\nconst registry = new Map()\n// pluginName -> Array<bindingId>\nconst plugins = new Map()\n// bindingId -> { comboStr, type }\nconst bindingIndex = new Map()\n\nlet paused = false\nlet nextId = 1\n\n// --- helpers ---\nfunction normalizeKey(key) {\n const original = String(key ?? '')\n // A lone space IS the spacebar key \u2014 `.trim()` would erase it to '', so guard\n // on the ORIGINAL before trimming. (The `space`/`spacebar` aliases resolve to\n // ' ' and land here; without this, `space` bindings collapse to no base key.)\n if (original === ' ') return ' '\n const trimmed = original.trim()\n const k = trimmed.toLowerCase()\n return keyAliases[k] ?? k\n}\n\nfunction comboKeyFromParts({ ctrl, shift, alt, meta, key, code, bareModifier }) {\n // Bare-modifier bindings get a sentinel namespace so they can't collide with\n // regular chord combo strings (`'alt+x'` vs `'__bare__:alt'`).\n if (bareModifier) return `__bare__:${bareModifier}`\n\n let s = ''\n if (ctrl) s += 'ctrl+'\n if (shift) s += 'shift+'\n if (alt) s += 'alt+'\n if (meta) s += 'meta+'\n\n // Code-based combos use a `code:KeyX` suffix instead of the key character.\n if (code) return s + 'code:' + code\n return s ? s + key : key\n}\n\nfunction comboKeyFromEvent(e) {\n // If the event's `key` is itself a modifier name, this is a bare-modifier\n // press/release. Match it via the bare-modifier sentinel regardless of which\n // other modifier flags are set (loose match \u2014 Alt fires `alt` even when held\n // alongside Ctrl).\n const bareMod = MODIFIER_KEY_TOKEN[e.key]\n if (bareMod) return `__bare__:${bareMod}`\n\n return comboKeyFromParts({\n ctrl: e.ctrlKey,\n shift: e.shiftKey,\n alt: e.altKey,\n meta: e.metaKey,\n key: normalizeKey(e.key),\n })\n}\n\n// Code-based lookup string for an event. Independent of `comboKeyFromEvent`\n// because consumers can register either key-based OR code-based bindings (or\n// both) for the same physical key combo, and we look up under whichever they\n// chose.\nfunction codeKeyFromEvent(e) {\n if (!e.code) return null\n\n let s = ''\n if (e.ctrlKey) s += 'ctrl+'\n if (e.shiftKey) s += 'shift+'\n if (e.altKey) s += 'alt+'\n if (e.metaKey) s += 'meta+'\n return s + 'code:' + e.code\n}\n\n\nfunction isModifierToken(token) {\n const t = normalizeKey(token)\n return modifierTokens.has(t)\n}\n\n\n// Matches `code:KeyX` (case-sensitive value). Used to extract code tokens BEFORE\n// the rest of the parser lowercases everything, since KeyboardEvent.code values\n// are camelCase (KeyA, Digit1, ArrowLeft) and must be preserved.\nconst CODE_TOKEN_RE = /(^|\\+)code:([A-Za-z][A-Za-z0-9_]*)/\n\nfunction parseHotkey(hotkeyStr) {\n const raw = String(hotkeyStr || '').trim()\n\n // Detect ' up' suffix case-insensitively so 'Alt UP' works the same as 'alt up'.\n const isUp = /\\s+up$/i.test(raw)\n const baseRaw = isUp ? raw.replace(/\\s+up$/i, '').trim() : raw\n\n // --- AHK-style prefix modifiers ---\n // Examples:\n // ^k => ctrl+k\n // !k => alt+k\n // +k => shift+k\n // #k => meta+k\n // ^!k => ctrl+alt+k\n // !code:KeyX => alt+code:KeyX\n //\n // We strip these BEFORE both the code-token extraction AND the lowercase\n // pass, because the AHK prefix chars (^ ! + #) are case-stable, and the\n // remainder may contain a case-sensitive `code:KeyX` token that must survive.\n const combo = {\n ctrl: false, shift: false, alt: false, meta: false,\n key: null, code: null, bareModifier: null,\n }\n\n let stripped = baseRaw\n while (stripped.length) {\n const mod = ahkPrefixMap.get(stripped[0]) ?? null\n if (!mod) break\n combo[mod] = true\n stripped = stripped.slice(1).trimStart()\n }\n\n // Pull out a code:XXX token before lowercasing; preserve the camelCase value.\n // Reject multiple code: tokens to avoid silent loss of intent.\n const codeMatches = stripped.match(/code:[A-Za-z][A-Za-z0-9_]*/g)\n if (codeMatches && codeMatches.length > 1) {\n throw new Error(\n `Invalid hotkey \"${hotkeyStr}\": multiple code: tokens not supported (got ${codeMatches.length}).`\n )\n }\n let codeValue = null\n const scrubbed = stripped.replace(CODE_TOKEN_RE, (_, sep, val) => {\n codeValue = val\n return sep + '__code__'\n })\n\n let cleanStr = scrubbed.toLowerCase()\n\n // Allow plus key:\n // - \"ctrl++\" => ctrl + \"+\"\n // - \"++\" => shift + \"+\" (AHK '+' prefix sets shift, remaining '+' becomes the base key)\n // `split('+')` produces empties; we keep that signal.\n const rawParts = cleanStr.split('+').map((p) => p.trim())\n const parts = rawParts.filter(Boolean)\n const impliedPlusKey = rawParts.some((p) => p === '') && cleanStr.includes('++')\n\n // Enforce: modifiers must come before the base key.\n // Example: \"ctrl+k\" \u2705 | \"k+ctrl\" \u274C\n // We ignore the special \"+ key\" case (ctrl++) since it has no explicit base token.\n if (!impliedPlusKey) {\n let seenBase = false\n for (const part of parts) {\n if (isModifierToken(part)) {\n if (seenBase) {\n throw new Error(\n `Invalid hotkey \"${hotkeyStr}\": modifiers must come before the base key (e.g. \"ctrl+k\", not \"k+ctrl\")`\n )\n }\n } else {\n if (!seenBase) seenBase = true\n }\n }\n }\n\n // Continue parsing the remaining tokens (words/symbols/aliases)\n for (const part of parts) {\n if (part === '__code__') {\n combo.code = codeValue\n continue\n }\n const actual = keyAliases[part] ?? part\n if (actual === 'ctrl') combo.ctrl = true\n else if (actual === 'shift') combo.shift = true\n else if (actual === 'alt') combo.alt = true\n else if (actual === 'meta') combo.meta = true\n else combo.key = normalizeKey(actual)\n }\n\n if (!combo.key && !combo.code && impliedPlusKey) combo.key = '+'\n\n // If no base key/code is set, this might be a bare-modifier binding ('alt' / 'ctrl').\n // Allow exactly ONE modifier with no other tokens. Multi-modifier bare bindings\n // (e.g. 'ctrl+alt') are intentionally rejected \u2014 add a base key for those.\n if (!combo.key && !combo.code) {\n const modCount =\n (combo.ctrl ? 1 : 0) + (combo.shift ? 1 : 0) +\n (combo.alt ? 1 : 0) + (combo.meta ? 1 : 0)\n\n if (modCount === 1) {\n combo.bareModifier =\n combo.ctrl ? 'ctrl' :\n combo.shift ? 'shift' :\n combo.alt ? 'alt' : 'meta'\n } else if (modCount > 1) {\n throw new Error(\n `Invalid hotkey \"${hotkeyStr}\": multi-modifier bare bindings not supported. Add a base key (e.g. \"ctrl+alt+x\").`\n )\n } else {\n throw new Error(`Invalid hotkey \"${hotkeyStr}\" (missing base key)`)\n }\n }\n\n return { combo, type: isUp ? 'keyup' : 'keydown', raw: hotkeyStr }\n}\n\n\nfunction isFromInputTarget(e) {\n const t = e?.target\n if (!t) return false\n const tag = String(t.tagName || '').toUpperCase()\n return (\n t.isContentEditable ||\n tag === 'INPUT' ||\n tag === 'TEXTAREA' ||\n tag === 'SELECT' ||\n t.getAttribute?.('role') === 'textbox'\n )\n}\n\n\n// --- binding model ---\n// options:\n// preventDefault, stopPropagation, stopImmediatePropagation\n// repeat (default false on keydown), once\n// when(e) -> boolean (gate)\n// allowIn(e) -> boolean (override ignoreInput)\n// priority (higher wins)\nfunction normalizeOptions(type, options) {\n const o = options ? { ...options } : {}\n if (type === 'keydown' && o.repeat == null) o.repeat = false\n if (o.priority == null) o.priority = 0\n return o\n}\n\n\nfunction ensureSlot(comboStr, type) {\n let typeMap = registry.get(comboStr)\n if (!typeMap) {\n typeMap = new Map()\n registry.set(comboStr, typeMap)\n }\n let arr = typeMap.get(type)\n if (!arr) {\n arr = []\n typeMap.set(type, arr)\n }\n return arr\n}\n\n\nfunction removeById(id) {\n const idx = bindingIndex.get(id)\n if (!idx) return false\n\n const { comboStr, type } = idx\n const typeMap = registry.get(comboStr)\n const arr = typeMap?.get(type)\n if (!arr?.length) {\n bindingIndex.delete(id)\n return false\n }\n\n const next = arr.filter((b) => b.id !== id)\n if (next.length) typeMap.set(type, next)\n else typeMap.delete(type)\n\n if (!typeMap.size) registry.delete(comboStr)\n bindingIndex.delete(id)\n return true\n}\n\n\n// --- public API: bind / unbind ---\n// bind returns an off() function; off.id is exposed for plugin bookkeeping.\n\n/**\n * Bind a hotkey combo string to a handler.\n *\n * @param {string} hotkeyStr e.g. `'ctrl+shift+k'`, `'meta+slash'`, `'alt+code:KeyX'`\n * @param {HotkeyHandler} handler\n * @param {string | null} [plugin] optional plugin/namespace tag for bulk cleanup\n * @param {BindOptions} [options]\n * @returns {BindOff} unbind function with `id` + `hotkey` props\n */\nfunction bind(hotkeyStr, handler, plugin = null, options = {}) {\n const { combo, type, raw } = parseHotkey(hotkeyStr)\n const comboStr = comboKeyFromParts(combo)\n const opts = normalizeOptions(type, options)\n\n const binding = {\n id: nextId++,\n comboStr,\n type,\n combo,\n handler,\n plugin,\n raw,\n options: opts,\n }\n\n ensureSlot(comboStr, type).push(binding)\n bindingIndex.set(binding.id, { comboStr, type })\n\n // Notify bind subscribers. Hook errors must never break the bind itself.\n if (bindHooks.length) {\n for (const hook of bindHooks) {\n try {\n hook({ combo, raw, options: opts, plugin, id: binding.id })\n } catch (err) {\n // eslint-disable-next-line no-console\n console.error('[hotkey-router] onBind hook threw:', err)\n }\n }\n }\n\n const off = () => removeById(binding.id)\n off.id = binding.id\n off.hotkey = raw\n return off\n}\n\n// Subscribe to bind events. The hook receives\n// { combo, raw, options, plugin, id }\n// after the binding is registered. Returns an unsubscribe function. Used by\n// extensions like `installReservationWarnings` from `hotkey-router/reservations`.\n\n/**\n * Subscribe to {@link bind} events. The hook is called after each successful\n * bind with a {@link BindEvent} payload.\n *\n * @param {(info: BindEvent) => void} hook\n * @returns {() => void} unsubscribe\n */\nfunction onBind(hook) {\n if (typeof hook !== 'function') {\n throw new TypeError('hotkeys.onBind(hook) expects a function')\n }\n bindHooks.push(hook)\n return () => {\n bindHooks = bindHooks.filter((h) => h !== hook)\n }\n}\n\n\n/**\n * Remove bindings for a combo string. Omit `handler` to remove all bindings\n * for the combo/type; pass a specific handler to remove just that one.\n *\n * @param {string} hotkeyStr\n * @param {HotkeyHandler} [handler]\n * @returns {void}\n */\nfunction unbind(hotkeyStr, handler) {\n const { combo, type } = parseHotkey(hotkeyStr)\n const comboStr = comboKeyFromParts(combo)\n const typeMap = registry.get(comboStr)\n if (!typeMap) return\n\n const arr = typeMap.get(type)\n if (!arr?.length) return\n\n if (!handler) {\n for (const b of arr) bindingIndex.delete(b.id)\n typeMap.delete(type)\n } else {\n const keep = []\n for (const b of arr) {\n if (b.handler === handler) bindingIndex.delete(b.id)\n else keep.push(b)\n }\n if (keep.length) typeMap.set(type, keep)\n else typeMap.delete(type)\n }\n\n if (!typeMap.size) registry.delete(comboStr)\n}\n\n\n// --- plugin system ---\n// registerPlugin returns unregister function.\n\n/**\n * Register a named plugin that bulk-binds a {@link HotkeyMap}. All bindings\n * are tagged with the plugin name so {@link unregisterPlugin} can clean them\n * up together without touching other plugins' bindings.\n *\n * @param {string} name plugin name (must be unique)\n * @param {HotkeyMap} hotkeyMap\n * @returns {() => void} unregister fn for this plugin\n */\nfunction registerPlugin(name, hotkeyMap) {\n if (plugins.has(name)) throw new Error(`Plugin \"${name}\" already registered`)\n const ids = []\n\n for (const [hotkeyStr, fnOrTuple] of Object.entries(hotkeyMap)) {\n const [fn, options] = Array.isArray(fnOrTuple) ? fnOrTuple : [fnOrTuple, {}]\n const off = bind(hotkeyStr, fn, name, options)\n ids.push(off.id)\n }\n\n plugins.set(name, ids)\n return () => unregisterPlugin(name)\n}\n\n/**\n * Remove all bindings owned by the named plugin. Silent no-op if no such\n * plugin is registered.\n *\n * @param {string} name\n * @returns {void}\n */\nfunction unregisterPlugin(name) {\n const ids = plugins.get(name)\n if (!ids) return\n for (const id of ids) removeById(id)\n plugins.delete(name)\n}\n\n// --- control ---\n\n/** Pause dispatch \u2014 bindings stay registered but no handlers fire. @returns {void} */\nfunction pause() {\n paused = true\n}\n\n/** Resume dispatch after a {@link pause}. @returns {void} */\nfunction resume() {\n paused = false\n}\n\n/**\n * Toggle whether handlers fire when the event target is an editable element\n * (input/textarea/contenteditable). Default `true`. Override per-binding via\n * `options.allowIn`.\n *\n * @param {boolean} [value]\n * @returns {void}\n */\nfunction ignoreInput(value = true) {\n ignoreEditable = !!value\n}\n\n// allow changing the default target (iframes/tests).\n// we intentionally do not auto-rebind to keep the core tiny.\n\n/**\n * Replace the default target (used by {@link init} when no `target` is\n * provided). Useful for iframes and tests. Does NOT auto-rebind existing\n * listeners \u2014 call {@link destroy} + {@link init} to re-attach.\n *\n * @param {EventTarget | null} target\n * @returns {void}\n */\nfunction setTarget(target) {\n defaultTarget = target\n}\n\n\n// --- dispatch (O(1) lookup) ---\nfunction pickWinner(bindings) {\n // Highest priority wins; ties -> newest wins (largest id).\n let best = null\n for (const b of bindings) {\n if (!best) best = b\n else if (b.options.priority > best.options.priority) best = b\n else if (b.options.priority === best.options.priority && b.id > best.id) best = b\n }\n return best\n}\n\n\nfunction handleEvent(type) {\n return (e) => {\n if (paused) return\n\n const fromInput = ignoreEditable && isFromInputTarget(e)\n const comboStr = comboKeyFromEvent(e)\n const codeStr = codeKeyFromEvent(e)\n\n // Look up under both lookup keys: key-based (`alt+x`) and code-based\n // (`alt+code:KeyX`). Same physical keypress, two valid binding shapes.\n // Merge candidates and let the existing priority/recency arbiter pick.\n const seen = new Set()\n const candidates = []\n for (const lookupKey of [comboStr, codeStr]) {\n if (!lookupKey || seen.has(lookupKey)) continue\n seen.add(lookupKey)\n\n const bindings = registry.get(lookupKey)?.get(type)\n if (!bindings?.length) continue\n\n for (const b of bindings) {\n const o = b.options\n if (fromInput && !o.allowIn?.(e)) continue\n if (type === 'keydown' && o.repeat === false && e.repeat) continue\n if (o.when && o.when(e) === false) continue\n candidates.push(b)\n }\n }\n if (!candidates.length) return\n\n const winner = pickWinner(candidates)\n const o = winner.options\n\n if (o.preventDefault) e.preventDefault()\n if (o.stopImmediatePropagation) e.stopImmediatePropagation()\n else if (o.stopPropagation) e.stopPropagation()\n\n winner.handler(e)\n\n if (o.once) removeById(winner.id)\n }\n}\n\n\n// --- listeners ---\nlet stopDown = null\nlet stopUp = null\n\n/**\n * Initialize the router by attaching `keydown`/`keyup` listeners to a target.\n * Re-initializes cleanly if already running (calls {@link destroy} first).\n * Auto-runs on `window` at module-load time in browser environments.\n *\n * @param {{ target?: EventTarget, capture?: boolean }} [opts]\n * @returns {void}\n */\nfunction init({ target = defaultTarget, capture = false } = {}) {\n if (!target) throw new Error('hotkeys.init() requires a target (e.g. window)')\n if (stopDown || stopUp) destroy()\n\n stopDown = on(target, 'keydown', handleEvent('keydown'), { capture })\n stopUp = on(target, 'keyup', handleEvent('keyup'), { capture })\n}\n\n\n/**\n * Tear down listeners and clear all bindings + plugins. Bind-hook\n * subscribers ({@link onBind}) deliberately survive \u2014 consumer\n * extensions should install once and live across `init`/`destroy` cycles.\n *\n * @returns {void}\n */\nfunction destroy() {\n stopDown?.()\n stopUp?.()\n stopDown = null\n stopUp = null\n registry.clear()\n plugins.clear()\n bindingIndex.clear()\n // Note: bindHooks deliberately survive destroy(). Consumer extensions\n // (reservations, future telemetry, etc.) install once at startup and\n // shouldn't have to re-attach when the router restarts. Tests that need\n // a clean slate should use the uninstall fn returned by their installer.\n}\n\n\n// --- programmatic trigger (tests / automation) ---\n//\n// Reverse map: 'alt' -> 'Alt' (the KeyboardEvent.key value for that bare modifier).\n// Used to populate the synthetic event's `.key` field so consumer handlers that\n// inspect `e.key` see the same value they'd get from a real event.\nconst BARE_MOD_TO_KEY = {\n ctrl: 'Control',\n shift: 'Shift',\n alt: 'Alt',\n meta: 'Meta',\n}\n\n/**\n * Programmatically fire any bindings for `hotkeyStr`. Used by tests and\n * automation; not a typical app-runtime path. Returns `true` if a binding\n * was found and invoked, `false` otherwise.\n *\n * @param {string} hotkeyStr\n * @param {{ type?: 'keydown' | 'keyup' }} [opts]\n * @returns {boolean}\n */\nfunction trigger(hotkeyStr, { type: forcedType } = {}) {\n const { combo, type } = parseHotkey(hotkeyStr)\n const comboStr = comboKeyFromParts(combo)\n const actualType = forcedType || type\n\n const bindings = registry.get(comboStr)?.get(actualType)\n if (!bindings?.length) return false\n\n // Fill `.key` from bareModifier when the binding is bare-modifier-only;\n // otherwise use combo.key. `.code` is set from combo.code for code-based bindings.\n const eventKey = combo.bareModifier\n ? BARE_MOD_TO_KEY[combo.bareModifier]\n : combo.key\n\n // For bare-modifier keyup, real KeyboardEvents have the corresponding\n // modifier flag FALSE (the modifier just released). Mirror that so\n // automation handlers that inspect e.altKey / e.ctrlKey / etc. see the\n // same value they'd see from a real event.\n let altKey = !!combo.alt\n let ctrlKey = !!combo.ctrl\n let shiftKey = !!combo.shift\n let metaKey = !!combo.meta\n if (combo.bareModifier && actualType === 'keyup') {\n if (combo.bareModifier === 'alt') altKey = false\n else if (combo.bareModifier === 'ctrl') ctrlKey = false\n else if (combo.bareModifier === 'shift') shiftKey = false\n else if (combo.bareModifier === 'meta') metaKey = false\n }\n\n const e = {\n type: actualType,\n key: eventKey,\n code: combo.code || null,\n ctrlKey, shiftKey, altKey, metaKey,\n repeat: false,\n preventDefault() { },\n stopPropagation() { },\n stopImmediatePropagation() { },\n target: null,\n }\n\n const winner = pickWinner(\n bindings.filter((b) => (b.options.when ? b.options.when(e) !== false : true))\n )\n if (!winner) return false\n\n winner.handler(e)\n if (winner.options.once) removeById(winner.id)\n return true\n}\n\n\n// Auto-init on window by default (browser only)\nif (defaultTarget) init()\n\nexport default {\n // lifecycle\n init,\n destroy,\n setTarget,\n\n // bindings\n bind,\n unbind,\n registerPlugin,\n unregisterPlugin,\n\n // extension hooks\n onBind,\n\n // control\n pause,\n resume,\n ignoreInput,\n\n // testing / automation\n trigger,\n}"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["o"]
|
|
7
7
|
}
|