@remcostoeten/use-shortcut 2.1.0 → 2.2.0
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 +79 -40
- package/dist/constants.d.ts +36 -0
- package/dist/constants.js +1 -0
- package/dist/constants.mjs +1 -0
- package/dist/formatter.d.ts +30 -0
- package/dist/formatter.js +1 -0
- package/dist/formatter.mjs +1 -0
- package/dist/index.d.ts +5 -468
- package/dist/parser.d.ts +47 -0
- package/dist/parser.js +1 -0
- package/dist/parser.mjs +1 -0
- package/dist/react.d.ts +79 -0
- package/dist/react.js +1 -0
- package/dist/react.mjs +1 -0
- package/dist/types-Cg7uyv1m.d.ts +314 -0
- package/package.json +46 -5
- package/dist/cli/index.mjs +0 -469
package/dist/react.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import {useRef,useMemo,useEffect}from'react';var m={MAC:"mac",WINDOWS:"windows",LINUX:"linux"},R=m,y=null;function M(){if(y)return y;if(typeof navigator>"u")return y=m.WINDOWS,y;let e=(navigator.userAgentData?.platform?.toLowerCase()??navigator.platform??navigator.userAgent??"").toLowerCase();return e.includes("mac")||e.includes("iphone")||e.includes("ipad")||e.includes("ipod")?(y=m.MAC,y):e.includes("linux")||e.includes("android")?(y=m.LINUX,y):(e.includes("win"),y=m.WINDOWS,y)}var p={META:"meta",CTRL:"ctrl",ALT:"alt",SHIFT:"shift"},z={command:p.META,cmd:p.META,"\u2318":p.META,meta:p.META,win:p.META,windows:p.META,super:p.META,mod:p.META,control:p.CTRL,ctrl:p.CTRL,"\u2303":p.CTRL,ctl:p.CTRL,alt:p.ALT,option:p.ALT,opt:p.ALT,"\u2325":p.ALT,shift:p.SHIFT,"\u21E7":p.SHIFT,shft:p.SHIFT},$={up:"ArrowUp",down:"ArrowDown",left:"ArrowLeft",right:"ArrowRight",home:"Home",end:"End",pageup:"PageUp",pagedown:"PageDown",enter:"Enter",return:"Enter",space:" ",spacebar:" ",tab:"Tab",backspace:"Backspace",delete:"Delete",del:"Delete",escape:"Escape",esc:"Escape",f1:"F1",f2:"F2",f3:"F3",f4:"F4",f5:"F5",f6:"F6",f7:"F7",f8:"F8",f9:"F9",f10:"F10",f11:"F11",f12:"F12",plus:"+",minus:"-",comma:",",period:".",slash:"/",backslash:"\\",bracket:"[",closebracket:"]"},j={[m.MAC]:{[p.META]:"\u2318",[p.CTRL]:"\u2303",[p.ALT]:"\u2325",[p.SHIFT]:"\u21E7"},[m.WINDOWS]:{[p.META]:"Ctrl",[p.CTRL]:"Ctrl",[p.ALT]:"Alt",[p.SHIFT]:"Shift"},[m.LINUX]:{[p.META]:"Super",[p.CTRL]:"Ctrl",[p.ALT]:"Alt",[p.SHIFT]:"Shift"}},D={[m.MAC]:[p.CTRL,p.ALT,p.SHIFT,p.META],[m.WINDOWS]:[p.META,p.ALT,p.SHIFT,p.CTRL],[m.LINUX]:[p.META,p.ALT,p.SHIFT,p.CTRL]};function W(t){return t===" "?"space":t.toLowerCase()}function L(t){let e=M(),o=t.toLowerCase().trim().split(/[\s+-]+/).filter(Boolean);if(o.length===0)throw new Error(`Invalid shortcut: "${t}"`);let n={meta:false,ctrl:false,alt:false,shift:false},s=o.pop();for(let i of o){let u=z[i];u?i==="mod"?e===R.MAC?n.meta=true:n.ctrl=true:n[u]=true:s=i+s;}let c=$[s]||s;return {modifiers:n,key:c.length===1?c.toLowerCase():c,original:t}}function gt(t){return {meta:t.metaKey,ctrl:t.ctrlKey,alt:t.altKey,shift:t.shiftKey}}function B(t,e){let r=gt(t),o=W(t.key),n=r.meta===e.modifiers.meta&&r.ctrl===e.modifiers.ctrl&&r.alt===e.modifiers.alt&&r.shift===e.modifiers.shift,s=o===W(e.key);return n&&s}function yt(t){return t==="ctrl"||t==="alt"||t==="shift"||t==="cmd"}function F(t){return t.split("+").map(e=>e.trim()).filter(Boolean)}function G(t){if(t)return t===true?{console:true}:t}function v(t){let e=G(t);return e?e.console!==false:false}function h(t,...e){v(t)&&console.log("[useShortcut]",...e);}function X(t,e){return {key:t.key,code:t.code,location:t.location,repeat:t.repeat,keyCode:"keyCode"in t?t.keyCode:void 0,which:"which"in t?t.which:void 0,combo:e,modifiers:{meta:t.metaKey,ctrl:t.ctrlKey,alt:t.altKey,shift:t.shiftKey}}}function bt(t,e,r){return e.has(t)?"match":r.has(t)?"wrong-order":"mismatch"}function Y(t,e,r){return t.map((o,n)=>{let s=e[n];if(!s)return {index:n,expected:o,status:"pending",tokens:[]};let c=new Set(F(o)),i=new Set(t.slice(n+1).flatMap(F)),u=F(s).map((l,b,k)=>({token:l,kind:yt(l)||b<k.length-1?"modifier":"key",status:bt(l,c,i)}));if(s===o)return {index:n,expected:o,actual:s,status:r||n<e.length-1?"match":"partial",tokens:u};let f=t.slice(n+1).includes(s)?"wrong-order":"mismatch";return {index:n,expected:o,actual:s,status:f,tokens:u}})}function J(t,e,r,o){if(o)return "matched";let n=t.slice(0,r);return n.length>0&&n.every(s=>s.status==="match"||s.status==="partial")?r<e?"partial":"mismatch":n.some(s=>s.status==="wrong-order")?"wrong-order":"mismatch"}function Q(t,e){if(!v(t))return;let r=G(t),o=[];if(r?.includeCode&&e.input.code&&o.push(`code=${e.input.code}`),r?.includeLocation&&o.push(`location=${String(e.input.location)}`),r?.includeKeyCode&&(typeof e.input.keyCode=="number"&&o.push(`keyCode=${String(e.input.keyCode)}`),typeof e.input.which=="number"&&o.push(`which=${String(e.input.which)}`)),e.attempts.length===0){console.log("[useShortcut]","key",e.input.combo,...o);return}for(let n of e.attempts)console.log("[useShortcut]",n.status.toUpperCase(),`${e.input.combo} -> ${n.combo}`,...o);}var V={ArrowUp:"\u2191",ArrowDown:"\u2193",ArrowLeft:"\u2190",ArrowRight:"\u2192",Home:"Home",End:"End",PageUp:"PgUp",PageDown:"PgDn"},Et={...V,Enter:"\u21A9",Tab:"\u21E5",Escape:"\u238B",Backspace:"\u232B",Delete:"\u2326"," ":"\u2423"},Tt={...V,Enter:"Enter",Tab:"Tab",Escape:"Esc",Backspace:"Backspace",Delete:"Del"," ":"Space"};function Z(t,e){let r=M(),o=L(t),n=j[r],s=D[r],c=[];for(let f of s)o.modifiers[f]&&c.push(n[f]);let i=Mt(o.key,r);c.push(i);let u=r===m.MAC?"":"+";return c.join(u)}function Mt(t,e){return (e===m.MAC?Et:Tt)[t]||t.toUpperCase()}function At(t){let e=M(),r=D[e],o=[];for(let n of r)n===p.CTRL&&t.ctrl&&o.push("ctrl"),n===p.ALT&&t.alt&&o.push("alt"),n===p.SHIFT&&t.shift&&o.push("shift"),n===p.META&&t.cmd&&o.push("cmd");return o}function tt(t,e){return [...At(t),e].join("+")}function et(t){return t.map(e=>Z(e)).join(" then ")}function A(t){let e=[];t.modifiers.ctrl&&e.push("ctrl"),t.modifiers.alt&&e.push("alt"),t.modifiers.shift&&e.push("shift"),t.modifiers.meta&&e.push("cmd");let r=t.key===" "||t.key==="Spacebar"?"space":t.key.toLowerCase();return [...e,r].join("+")}function N(t){let e=[];t.ctrlKey&&e.push("ctrl"),t.altKey&&e.push("alt"),t.shiftKey&&e.push("shift"),t.metaKey&&e.push("cmd");let r=t.key===" "||t.key==="Spacebar"?"space":t.key.toLowerCase();return [...e,r].join("+")}function rt(t){return N(t)}function ot(t,e){if(t.length>e.length)return false;for(let r=0;r<t.length;r+=1)if(t[r]!==e[r])return false;return true}function nt(t,e){let r=t.map(A),o=e.map(A),n=r.join(" "),s=o.join(" ");return n===s?"exact":ot(r,o)||ot(o,r)?"sequence-prefix":null}function st(t,e){let r=t.options.conflictWarnings??true;if(t.options.onConflict){t.options.onConflict(e);return}r&&console.warn(`[useShortcut] Conflict detected (${e.reason}) between "${e.combo}" and "${e.existingCombo}"`);}var P=new Set(["INPUT","TEXTAREA","SELECT"]),it={input:t=>{if(!(t.target instanceof HTMLElement))return false;let e=t.target;return P.has(e.tagName)},editable:t=>t.target instanceof HTMLElement?t.target.isContentEditable:false,typing:t=>{if(!(t.target instanceof HTMLElement))return false;let e=t.target;return P.has(e.tagName)||e.isContentEditable},modal:()=>typeof document>"u"||typeof document.querySelector!="function"?false:document.querySelector('[data-modal="true"], [role="dialog"]')!==null,disabled:t=>{if(!(t.target instanceof HTMLElement))return false;let e=t.target;return e.hasAttribute("disabled")||e.getAttribute("aria-disabled")==="true"}};function ct(t,e){return e?typeof e=="function"?e(t):Array.isArray(e)?e.some(r=>it[r]?.(t)):it[e]?.(t)??false:false}function C(t){return t?(Array.isArray(t)?t:[t]).map(e=>e.trim()).filter(Boolean):[]}function ut(t,e){if(t.size===0)return true;for(let r of t)if(e.has(r))return true;return false}function at(t){let e=t.key.toLowerCase();return e==="shift"||e==="control"||e==="alt"||e==="meta"}function Ct(t){return t.length<=1?t:[...t].sort((e,r)=>r.priority!==e.priority?r.priority-e.priority:e.id-r.id)}function wt(t,e){let r=t.options;if(r.disabled||r.eventFilter&&!r.eventFilter(e))return;let o=rt(e),n=X(e,o),s=[],c=new Set,i=t.debugListeners.size>0||v(r.debug)||[...t.listeners.values()].some(l=>l.some(b=>b.attemptCallbacks.size>0)),u=t.firstStepIndex.get(o);if(u)for(let l of u)c.add(l);for(let l of t.activeSequenceCombos)c.add(l);if(i)for(let l of t.listeners.keys())c.add(l);for(let l of c){let b=t.listeners.get(l);if(!b)continue;let k=Ct(b);for(let a of k){if(!a.isEnabled||!ut(a.scopes,t.activeScopes))continue;if(r.ignoreInputs!==false&&!a.except){let S=e.target;if(S&&(P.has(S.tagName)||S.isContentEditable))continue}if(ct(e,a.except)){h(r.debug,"Skipped due to except condition:",l);continue}let I=a.parsedSteps[a.progress],E=Date.now();a.progress>0&&E-a.lastMatchedAt>a.sequenceTimeout&&(a.progress=0),a.debugHistory.length>0&&E-a.lastDebugAt>a.sequenceTimeout&&(a.debugHistory=[]);let H=a.progress,T=false;B(e,I)?(a.progress+=1,a.lastMatchedAt=E,a.progress===a.parsedSteps.length&&(T=true,a.progress=0)):a.progress>0&&B(e,a.parsedSteps[0])?(a.progress=1,a.lastMatchedAt=E):a.progress=0,a.lastDebugAt=E,a.debugHistory.push(o),a.debugHistory.length>a.expectedSteps.length&&a.debugHistory.shift();let g=a.debugHistory.slice(-a.expectedSteps.length),_=Y(a.expectedSteps,g,T),O={combo:a.combo,display:a.display,description:a.description,status:J(_,a.expectedSteps.length,g.length,T),matched:T,progress:a.progress,expectedSteps:a.expectedSteps,actualSteps:g,stepIndex:H,input:n,steps:_};s.push(O);for(let S of a.attemptCallbacks)S(T,e,O);if(!T)continue;h(r.debug,"MATCHED:",l),a.preventDefault&&e.preventDefault(),a.stopPropagation&&e.stopPropagation();let d=()=>a.userHandler(e);if(a.delay>0?(h(r.debug,"Delaying execution by",a.delay,"ms"),setTimeout(d,a.delay)):d(),a.stopOnMatch)break}b.some(a=>a.progress>0)?t.activeSequenceCombos.add(l):t.activeSequenceCombos.delete(l);}let f={input:n,attempts:s};if(t.debugListeners.size>0)for(let l of t.debugListeners)l(f);Q(r.debug,f);}function pt(t){if(t.listener)return;let e=t.options.target??(typeof window<"u"?window:null);if(!e)return;let r=t.options.eventType??"keydown",o=n=>wt(t,n);e.addEventListener(r,o),t.listener=o,t.listenerTarget=e,t.listenerEventType=r,h(t.options.debug,"Listener attached");}function lt(t){!t.listener||!t.listenerTarget||(t.listenerTarget.removeEventListener(t.listenerEventType,t.listener),t.listener=null,t.listenerTarget=null,h(t.options.debug,"Listener detached"));}function U(t,e,r={},o){let{options:n,except:s}=t,c=t.steps;if(c.length===0)throw new Error("[useShortcut] No key specified. Use .key() to set the action key.");let i=c.map(d=>L(d)),u=i.map(A).join(" "),f=et(c),l=n.debug??false,b=s??r.except;for(let[d,S]of o.listeners.entries())for(let w of S){if(d===u)continue;let x=nt(i,w.parsedSteps);x&&st(o,{combo:u,existingCombo:d,reason:x});}let k=!r.disabled&&!n.disabled,a=r.delay??n.delay??0,I=r.sequenceTimeout??n.sequenceTimeout??800,E=new Set(C(t.scopes??r.scopes)),H=i.map(A),T=new Set;h(l,"Registering:",u,"\u2192",f,{parsedSteps:i,except:!!b,scopes:[...E]});let g={id:o.nextId++,userHandler:e,isEnabled:k,combo:u,display:f,description:r.description,attemptCallbacks:T,parsedSteps:i,expectedSteps:H,scopes:E,progress:0,lastMatchedAt:0,debugHistory:[],lastDebugAt:0,except:b,delay:a,sequenceTimeout:I,preventDefault:r.preventDefault!==false,stopPropagation:r.stopPropagation??false,stopOnMatch:r.stopOnMatch??false,priority:r.priority??0},_=o.listeners.get(u);if(_)_.push(g);else {o.listeners.set(u,[g]);let d=A(i[0]),S=o.firstStepIndex.get(d);S?S.add(u):o.firstStepIndex.set(d,new Set([u]));}return pt(o),{unbind:()=>{let d=o.listeners.get(u);if(!d)return;let S=d.filter(w=>w.id!==g.id);if(S.length===0){o.listeners.delete(u),o.activeSequenceCombos.delete(u);let w=A(i[0]),x=o.firstStepIndex.get(w);x&&(x.delete(u),x.size===0&&o.firstStepIndex.delete(w)),h(l,"Unregistered:",u);}else o.listeners.set(u,S);o.listeners.size===0&<(o);},display:f,combo:u,trigger:()=>e(new KeyboardEvent(o.options.eventType??"keydown")),get isEnabled(){return g.isEnabled},enable:()=>{g.isEnabled=true;},disable:()=>{g.isEnabled=false;},onAttempt:d=>(g.attemptCallbacks.add(d),()=>g.attemptCallbacks.delete(d))}}function ft(t){return (e={})=>new Promise((r,o)=>{let n=e.target??t.target??(typeof window<"u"?window:null),s=e.eventType??t.eventType??"keydown";if(!n){o(new Error("[useShortcut] Cannot record shortcut without a target."));return}let c,i=f=>{let l=f;at(l)||(l.preventDefault(),n.removeEventListener(s,i),c&&clearTimeout(c),r(N(l)));};n.addEventListener(s,i);let u=e.timeoutMs;u&&u>0&&(c=setTimeout(()=>{n.removeEventListener(s,i),o(new Error(`[useShortcut] Recording timed out after ${u}ms.`));},u));})}var xt=new Set(["ctrl","shift","alt","cmd","mod"]);function dt(t={}){let e={listeners:new Map,firstStepIndex:new Map,activeSequenceCombos:new Set,options:t,activeScopes:new Set(C(t.activeScopes)),nextId:1,debugListeners:new Set,listener:null,listenerTarget:null,listenerEventType:t.eventType??"keydown"};h(t.debug,"Builder created with options:",t);function r(n){return new Proxy({},{get(s,c){if(c==="__debug")return n.options.debug;if(xt.has(c)){let i=M(),u=c==="mod"?i===R.MAC?"cmd":"ctrl":c,f={...n,modifiers:{...n.modifiers,[u]:true}};return h(n.options.debug,`Chain: +${c} \u2192`,f.modifiers),r(f)}if(c==="in")return i=>{let u=[...C(n.scopes),...C(i)],f={...n,scopes:u};return r(f)};if(c==="setScopes")return i=>{e.activeScopes=new Set(C(i));};if(c==="enableScope")return i=>{i?.trim()&&e.activeScopes.add(i.trim());};if(c==="disableScope")return i=>{i?.trim()&&e.activeScopes.delete(i.trim());};if(c==="getScopes")return ()=>[...e.activeScopes];if(c==="isScopeActive")return i=>e.activeScopes.has(i);if(c==="onDebug")return i=>(e.debugListeners.add(i),()=>e.debugListeners.delete(i));if(c==="record")return ft(e.options);if(c==="key")return i=>{let u=tt(n.modifiers,i),f={...n,modifiers:{},steps:[...n.steps,u]};return h(n.options.debug,`Chain: .key("${i}")`),r(f)};if(c==="then")return i=>{let u=String(i).trim().toLowerCase();if(!u)throw new Error("[useShortcut] .then() requires a non-empty key or shortcut step.");let f={...n,steps:[...n.steps,u]};return h(n.options.debug,`Chain: .then("${u}")`),r(f)};if(c==="except")return i=>{let u={...n,except:i};return h(n.options.debug,"Chain: .except()",i),r(u)};if(c==="on")return (i,u)=>U(n,i,u,e);if(c==="handle")return i=>{let{handler:u,...f}=i;return U(n,u,f,e)}}})}return {builder:r({modifiers:{},steps:[],options:t}),registry:e}}function _t(t,e){if(Array.isArray(t)&&Array.isArray(e)){if(t.length!==e.length)return false;for(let r=0;r<t.length;r+=1)if(t[r]!==e[r])return false;return true}return !Array.isArray(t)&&!Array.isArray(e)?t===e:false}function Rt(t,e){let r=Object.keys(t),o=Object.keys(e);if(r.length!==o.length)return false;for(let n of r){let s=t[n],c=e[n];if(!c||!_t(s.keys,c.keys)||s.handler!==c.handler||s.options!==c.options)return false}return true}function Dt(t){if(Array.isArray(t))return t.map(r=>r.trim()).filter(Boolean);let e=t.trim();return e?e.includes(" then ")?e.split(/\s+then\s+/i).map(r=>r.trim()).filter(Boolean):e.includes(" ")&&!e.includes("+")?e.split(/\s+/).map(r=>r.trim()).filter(Boolean):[e]:[]}function Lt(t,e){let r=e.toLowerCase().split("+").map(s=>s.trim()).filter(Boolean);if(r.length===0)throw new Error("[useShortcutMap] Invalid step: empty shortcut step");let o=r.pop(),n=t;for(let s of r){if(s==="ctrl"||s==="control"){n=n.ctrl;continue}if(s==="shift"){n=n.shift;continue}if(s==="alt"||s==="option"){n=n.alt;continue}if(s==="cmd"||s==="command"||s==="meta"){n=n.cmd;continue}if(s==="mod"){n=n.mod;continue}throw new Error(`[useShortcutMap] Unsupported modifier token "${s}" in step "${e}"`)}return n.key(o)}function mt(t,e){let r={};for(let o of Object.keys(e)){let n=e[o],s=Dt(n.keys);if(s.length===0)throw new Error(`[useShortcutMap] Shortcut "${String(o)}" has no key steps`);let c=Lt(t,s[0]);for(let i of s.slice(1))c=c.then(i);r[o]=c.on(n.handler,n.options);}return r}function ht(t={}){let e=useRef(t);e.current=t;let{builder:r,registry:o}=useMemo(()=>dt(e.current),[]);return useEffect(()=>{if(o.options=e.current,e.current.activeScopes!==void 0){let n=Array.isArray(e.current.activeScopes)?e.current.activeScopes:[e.current.activeScopes];o.activeScopes=new Set(n.map(s=>s.trim()).filter(Boolean));}},[o,t]),useEffect(()=>()=>{o.listeners.clear(),o.firstStepIndex.clear(),o.activeSequenceCombos.clear(),o.listener&&o.listenerTarget&&(o.listenerTarget.removeEventListener(o.listenerEventType,o.listener),o.listener=null,o.listenerTarget=null);},[o]),r}function vt(t,e={}){let r=ht(e),o=useRef(t);Rt(o.current,t)||(o.current=t);let n=o.current,s=useRef({});return useEffect(()=>{let c=mt(r,n),i=s.current;for(let u of Object.keys(i))delete i[u];return Object.assign(i,c),()=>{for(let u of Object.values(c))u.unbind();for(let u of Object.keys(i))delete i[u];}},[r,n]),s.current}function St(){let t=[];return {add:(...e)=>{t.push(...e);},addMany:e=>{if(Array.isArray(e)){t.push(...e);return}t.push(...Object.values(e));},unbindAll:()=>{for(let e of t)e.unbind();t.length=0;},clear:()=>{t.length=0;},getResults:()=>[...t]}}function Pt(){let t=useRef(null);return t.current||(t.current=St()),t.current}export{St as createShortcutGroup,mt as registerShortcutMap,ht as useShortcut,Pt as useShortcutGroup,vt as useShortcutMap};
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/** Lowercase letter keys a-z */
|
|
2
|
+
type AlphaKey = "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z";
|
|
3
|
+
/** Number keys 0-9 */
|
|
4
|
+
type NumericKey = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9";
|
|
5
|
+
/** Function keys F1-F12 */
|
|
6
|
+
type FunctionKey = "f1" | "f2" | "f3" | "f4" | "f5" | "f6" | "f7" | "f8" | "f9" | "f10" | "f11" | "f12";
|
|
7
|
+
/** Arrow and navigation keys */
|
|
8
|
+
type NavigationKey = "up" | "down" | "left" | "right" | "arrowup" | "arrowdown" | "arrowleft" | "arrowright" | "home" | "end" | "pageup" | "pagedown";
|
|
9
|
+
/** Special action keys like Enter, Escape, Tab */
|
|
10
|
+
type SpecialKey = "enter" | "return" | "escape" | "esc" | "space" | "tab" | "backspace" | "delete" | "del" | "insert";
|
|
11
|
+
/** Symbol and punctuation keys */
|
|
12
|
+
type SymbolKey = "minus" | "plus" | "equal" | "equals" | "bracketleft" | "bracketright" | "backslash" | "slash" | "/" | "comma" | "period" | "semicolon" | "quote" | "backtick";
|
|
13
|
+
/**
|
|
14
|
+
* All valid action keys that can be used with `.key()`
|
|
15
|
+
* @example $.mod.key("s") // "s" is an ActionKey
|
|
16
|
+
*/
|
|
17
|
+
type ActionKey = AlphaKey | NumericKey | FunctionKey | NavigationKey | SpecialKey | SymbolKey;
|
|
18
|
+
/** Modifier key names used in the chainable API */
|
|
19
|
+
type ModifierName = "ctrl" | "shift" | "alt" | "cmd" | "mod";
|
|
20
|
+
/** Internal modifier state flags */
|
|
21
|
+
type ModifierFlags = {
|
|
22
|
+
ctrl: boolean;
|
|
23
|
+
shift: boolean;
|
|
24
|
+
alt: boolean;
|
|
25
|
+
cmd: boolean;
|
|
26
|
+
};
|
|
27
|
+
/** Modifier key state from a keyboard event */
|
|
28
|
+
type ModifierState = {
|
|
29
|
+
meta: boolean;
|
|
30
|
+
ctrl: boolean;
|
|
31
|
+
alt: boolean;
|
|
32
|
+
shift: boolean;
|
|
33
|
+
};
|
|
34
|
+
/** Result of parsing a shortcut string */
|
|
35
|
+
type ParsedShortcut = {
|
|
36
|
+
modifiers: ModifierState;
|
|
37
|
+
key: string;
|
|
38
|
+
original: string;
|
|
39
|
+
};
|
|
40
|
+
type EmptyModifiers = {};
|
|
41
|
+
/**
|
|
42
|
+
* Handler function called when a shortcut is triggered
|
|
43
|
+
* @param event - The keyboard event that triggered the shortcut
|
|
44
|
+
*/
|
|
45
|
+
type ShortcutHandler = (event: KeyboardEvent) => void;
|
|
46
|
+
/**
|
|
47
|
+
* Custom predicate for excluding shortcuts in certain conditions
|
|
48
|
+
* @param event - The keyboard event to evaluate
|
|
49
|
+
* @returns `true` to skip the shortcut, `false` to allow it
|
|
50
|
+
*/
|
|
51
|
+
type ExceptPredicate = (event: KeyboardEvent) => boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Built-in exception presets for common scenarios
|
|
54
|
+
* - "input" - Skip when focused on input, textarea, or select
|
|
55
|
+
* - "editable" - Skip when focused on contentEditable elements
|
|
56
|
+
* - "typing" - Skip in any text input context (combines input + editable)
|
|
57
|
+
* - "modal" - Skip when a modal/dialog is open (checks [data-modal] or [role="dialog"])
|
|
58
|
+
* - "disabled" - Skip when focused element is disabled
|
|
59
|
+
*/
|
|
60
|
+
type ExceptPreset = "input" | "editable" | "typing" | "modal" | "disabled";
|
|
61
|
+
/** Scope selector used to enable/disable subsets of shortcuts at runtime. */
|
|
62
|
+
type ShortcutScope = string | string[];
|
|
63
|
+
/** Conflict metadata emitted when two registered shortcuts overlap. */
|
|
64
|
+
type ShortcutConflict = {
|
|
65
|
+
combo: string;
|
|
66
|
+
existingCombo: string;
|
|
67
|
+
reason: "exact" | "sequence-prefix";
|
|
68
|
+
};
|
|
69
|
+
/** High-level match status for one shortcut attempt against the current keyboard input. */
|
|
70
|
+
type ShortcutAttemptStatus = "matched" | "partial" | "wrong-order" | "mismatch";
|
|
71
|
+
/** Token-level verdict for modifiers and keys inside debug attempt payloads. */
|
|
72
|
+
type ShortcutDebugTokenStatus = "match" | "wrong-order" | "mismatch";
|
|
73
|
+
/** Debug metadata for one expected token in a shortcut step. */
|
|
74
|
+
type ShortcutDebugToken = {
|
|
75
|
+
token: string;
|
|
76
|
+
kind: "modifier" | "key";
|
|
77
|
+
status: ShortcutDebugTokenStatus;
|
|
78
|
+
};
|
|
79
|
+
/** Debug metadata for one step in a combo or multi-step shortcut sequence. */
|
|
80
|
+
type ShortcutDebugStep = {
|
|
81
|
+
index: number;
|
|
82
|
+
expected: string;
|
|
83
|
+
actual?: string;
|
|
84
|
+
status: "match" | "partial" | "pending" | "wrong-order" | "mismatch";
|
|
85
|
+
tokens: ShortcutDebugToken[];
|
|
86
|
+
};
|
|
87
|
+
/** Normalized view of the keyboard input that triggered debug processing. */
|
|
88
|
+
type ShortcutDebugInput = {
|
|
89
|
+
key: string;
|
|
90
|
+
code: string;
|
|
91
|
+
location: number;
|
|
92
|
+
repeat: boolean;
|
|
93
|
+
keyCode?: number;
|
|
94
|
+
which?: number;
|
|
95
|
+
combo: string;
|
|
96
|
+
modifiers: ModifierState;
|
|
97
|
+
};
|
|
98
|
+
/** Per-shortcut debug payload describing how one registered shortcut was evaluated. */
|
|
99
|
+
type ShortcutAttemptDebugEvent = {
|
|
100
|
+
combo: string;
|
|
101
|
+
display: string;
|
|
102
|
+
description?: string;
|
|
103
|
+
status: ShortcutAttemptStatus;
|
|
104
|
+
matched: boolean;
|
|
105
|
+
progress: number;
|
|
106
|
+
expectedSteps: string[];
|
|
107
|
+
actualSteps: string[];
|
|
108
|
+
stepIndex: number;
|
|
109
|
+
input: ShortcutDebugInput;
|
|
110
|
+
steps: ShortcutDebugStep[];
|
|
111
|
+
};
|
|
112
|
+
/** Global debug payload emitted for every processed keyboard event. */
|
|
113
|
+
type ShortcutDebugEvent = {
|
|
114
|
+
input: ShortcutDebugInput;
|
|
115
|
+
attempts: ShortcutAttemptDebugEvent[];
|
|
116
|
+
};
|
|
117
|
+
/** Runtime debug configuration for console/debug-stream metadata. */
|
|
118
|
+
type ShortcutDebugOptions = {
|
|
119
|
+
/** Log shortcut attempts to the console (default: true) */
|
|
120
|
+
console?: boolean;
|
|
121
|
+
/** Include `KeyboardEvent.code` in console output */
|
|
122
|
+
includeCode?: boolean;
|
|
123
|
+
/** Include `KeyboardEvent.location` in console output */
|
|
124
|
+
includeLocation?: boolean;
|
|
125
|
+
/** Include deprecated numeric key metadata in console output when available */
|
|
126
|
+
includeKeyCode?: boolean;
|
|
127
|
+
};
|
|
128
|
+
/**
|
|
129
|
+
* Options for shortcut handler registration
|
|
130
|
+
*/
|
|
131
|
+
type HandlerOptions = {
|
|
132
|
+
/** Prevent the browser's default action (default: `true`) */
|
|
133
|
+
preventDefault?: boolean;
|
|
134
|
+
/** Stop event propagation */
|
|
135
|
+
stopPropagation?: boolean;
|
|
136
|
+
/** Delay handler execution in milliseconds */
|
|
137
|
+
delay?: number;
|
|
138
|
+
/** Description for documentation/debugging */
|
|
139
|
+
description?: string;
|
|
140
|
+
/** Disable this specific shortcut */
|
|
141
|
+
disabled?: boolean;
|
|
142
|
+
/** Conditions to skip the shortcut */
|
|
143
|
+
except?: ExceptPreset | ExceptPreset[] | ExceptPredicate;
|
|
144
|
+
/** Required named scopes that must be active */
|
|
145
|
+
scopes?: ShortcutScope;
|
|
146
|
+
/** Timeout in ms for multi-step sequences */
|
|
147
|
+
sequenceTimeout?: number;
|
|
148
|
+
/** Higher priority handlers run first (default: 0) */
|
|
149
|
+
priority?: number;
|
|
150
|
+
/** Stop evaluating other handlers for this combo when matched */
|
|
151
|
+
stopOnMatch?: boolean;
|
|
152
|
+
};
|
|
153
|
+
/**
|
|
154
|
+
* Result object returned when registering a shortcut
|
|
155
|
+
* Provides control over the shortcut and display information
|
|
156
|
+
*/
|
|
157
|
+
type ShortcutResult = {
|
|
158
|
+
/** Remove the keyboard listener */
|
|
159
|
+
unbind: () => void;
|
|
160
|
+
/** Platform-aware display string (e.g., "⌘S" on Mac, "Ctrl+S" on Windows) */
|
|
161
|
+
display: string;
|
|
162
|
+
/** Normalized combo string (e.g., "cmd+s" or "g d") */
|
|
163
|
+
combo: string;
|
|
164
|
+
/** Programmatically trigger the shortcut handler */
|
|
165
|
+
trigger: () => void;
|
|
166
|
+
/** Whether the shortcut is currently enabled */
|
|
167
|
+
isEnabled: boolean;
|
|
168
|
+
/** Enable the shortcut (after being disabled) */
|
|
169
|
+
enable: () => void;
|
|
170
|
+
/** Temporarily disable the shortcut */
|
|
171
|
+
disable: () => void;
|
|
172
|
+
/** Subscribe to shortcut attempt events (useful for visual feedback) */
|
|
173
|
+
onAttempt?: (callback: (matched: boolean, event: KeyboardEvent, details?: ShortcutAttemptDebugEvent) => void) => () => void;
|
|
174
|
+
};
|
|
175
|
+
/**
|
|
176
|
+
* Chainable modifier builder with type-safe exhaustion
|
|
177
|
+
* Each modifier can only be used once in a chain
|
|
178
|
+
*/
|
|
179
|
+
type ModifierChain<Used extends Partial<ModifierFlags>> = {
|
|
180
|
+
ctrl: Used["ctrl"] extends true ? never : ModifierChain<Used & {
|
|
181
|
+
ctrl: true;
|
|
182
|
+
}>;
|
|
183
|
+
shift: Used["shift"] extends true ? never : ModifierChain<Used & {
|
|
184
|
+
shift: true;
|
|
185
|
+
}>;
|
|
186
|
+
alt: Used["alt"] extends true ? never : ModifierChain<Used & {
|
|
187
|
+
alt: true;
|
|
188
|
+
}>;
|
|
189
|
+
cmd: Used["cmd"] extends true ? never : ModifierChain<Used & {
|
|
190
|
+
cmd: true;
|
|
191
|
+
}>;
|
|
192
|
+
mod: Used["cmd"] extends true ? never : ModifierChain<Used & {
|
|
193
|
+
cmd: true;
|
|
194
|
+
}>;
|
|
195
|
+
key: <K extends ActionKey>(key: K) => KeyChain<K>;
|
|
196
|
+
in: (scopes: ShortcutScope) => ModifierChain<Used>;
|
|
197
|
+
};
|
|
198
|
+
/**
|
|
199
|
+
* Chain state after calling `.key()` - ready to attach a handler
|
|
200
|
+
*/
|
|
201
|
+
type KeyChain<Key extends string> = {
|
|
202
|
+
/** Attach a handler to this shortcut */
|
|
203
|
+
on: (handler: ShortcutHandler, options?: HandlerOptions) => ShortcutResult;
|
|
204
|
+
/** Attach a handler with inline options */
|
|
205
|
+
handle: (options: HandlerOptions & {
|
|
206
|
+
handler: ShortcutHandler;
|
|
207
|
+
}) => ShortcutResult;
|
|
208
|
+
/** Add exception conditions before attaching handler */
|
|
209
|
+
except: (condition: ExceptPreset | ExceptPreset[] | ExceptPredicate) => KeyChainWithExcept<Key>;
|
|
210
|
+
/** Add required named scopes */
|
|
211
|
+
in: (scopes: ShortcutScope) => KeyChain<Key>;
|
|
212
|
+
/** Add the next step in a sequence */
|
|
213
|
+
then: <K extends ActionKey | string>(key: K) => KeyChain<`${Key} ${K}`>;
|
|
214
|
+
};
|
|
215
|
+
/**
|
|
216
|
+
* Chain state after calling `.except()` - ready to attach handler
|
|
217
|
+
*/
|
|
218
|
+
type KeyChainWithExcept<Key extends string> = {
|
|
219
|
+
on: (handler: ShortcutHandler, options?: Omit<HandlerOptions, "except">) => ShortcutResult;
|
|
220
|
+
in: (scopes: ShortcutScope) => KeyChainWithExcept<Key>;
|
|
221
|
+
then: <K extends ActionKey | string>(key: K) => KeyChainWithExcept<`${Key} ${K}`>;
|
|
222
|
+
};
|
|
223
|
+
/** Options for `ShortcutBuilder.record()` and low-level recording flows. */
|
|
224
|
+
type ShortcutRecordingOptions = {
|
|
225
|
+
target?: HTMLElement | Window | null;
|
|
226
|
+
eventType?: "keydown" | "keyup";
|
|
227
|
+
timeoutMs?: number;
|
|
228
|
+
};
|
|
229
|
+
/**
|
|
230
|
+
* The main shortcut builder interface returned by `useShortcut()`
|
|
231
|
+
*/
|
|
232
|
+
type ShortcutBuilder = ModifierChain<EmptyModifiers> & {
|
|
233
|
+
ctrl: ModifierChain<{
|
|
234
|
+
ctrl: true;
|
|
235
|
+
}>;
|
|
236
|
+
shift: ModifierChain<{
|
|
237
|
+
shift: true;
|
|
238
|
+
}>;
|
|
239
|
+
alt: ModifierChain<{
|
|
240
|
+
alt: true;
|
|
241
|
+
}>;
|
|
242
|
+
cmd: ModifierChain<{
|
|
243
|
+
cmd: true;
|
|
244
|
+
}>;
|
|
245
|
+
mod: ModifierChain<{
|
|
246
|
+
cmd: true;
|
|
247
|
+
}>;
|
|
248
|
+
key: <K extends ActionKey>(key: K) => KeyChain<K>;
|
|
249
|
+
/** Set required scopes for upcoming chain calls */
|
|
250
|
+
in: (scopes: ShortcutScope) => ShortcutBuilder;
|
|
251
|
+
/** Update active scopes at runtime */
|
|
252
|
+
setScopes: (scopes: ShortcutScope) => void;
|
|
253
|
+
/** Enable one scope */
|
|
254
|
+
enableScope: (scope: string) => void;
|
|
255
|
+
/** Disable one scope */
|
|
256
|
+
disableScope: (scope: string) => void;
|
|
257
|
+
/** Return currently active scopes */
|
|
258
|
+
getScopes: () => string[];
|
|
259
|
+
/** Check if a scope is active */
|
|
260
|
+
isScopeActive: (scope: string) => boolean;
|
|
261
|
+
/** Subscribe to every keyboard input evaluated by this shortcut registry */
|
|
262
|
+
onDebug: (callback: (event: ShortcutDebugEvent) => void) => () => void;
|
|
263
|
+
/** Record the next key combo */
|
|
264
|
+
record: (options?: ShortcutRecordingOptions) => Promise<string>;
|
|
265
|
+
};
|
|
266
|
+
/**
|
|
267
|
+
* Options for the `useShortcut` hook
|
|
268
|
+
*/
|
|
269
|
+
type UseShortcutOptions = {
|
|
270
|
+
/** Enable debug logging to console or configure structured debug output */
|
|
271
|
+
debug?: boolean | ShortcutDebugOptions;
|
|
272
|
+
/** Global delay for all handlers in milliseconds */
|
|
273
|
+
delay?: number;
|
|
274
|
+
/** Skip shortcuts when focused on input elements (default: `true`) */
|
|
275
|
+
ignoreInputs?: boolean;
|
|
276
|
+
/** Target element for keyboard listeners (default: `window`) */
|
|
277
|
+
target?: HTMLElement | Window | null;
|
|
278
|
+
/** Keyboard event type to listen for (default: "keydown") */
|
|
279
|
+
eventType?: "keydown" | "keyup";
|
|
280
|
+
/** Globally disable all shortcuts from this hook */
|
|
281
|
+
disabled?: boolean;
|
|
282
|
+
/** Active named scopes. Shortcuts with scopes only run when at least one matches. */
|
|
283
|
+
activeScopes?: ShortcutScope;
|
|
284
|
+
/** Global timeout in ms for sequence completion */
|
|
285
|
+
sequenceTimeout?: number;
|
|
286
|
+
/** Warn when conflicting shortcuts are registered (default: true) */
|
|
287
|
+
conflictWarnings?: boolean;
|
|
288
|
+
/** Custom conflict callback */
|
|
289
|
+
onConflict?: (conflict: ShortcutConflict) => void;
|
|
290
|
+
/** Global event filter; return false to skip all shortcuts for the event */
|
|
291
|
+
eventFilter?: (event: KeyboardEvent) => boolean;
|
|
292
|
+
};
|
|
293
|
+
/** Single shortcut-map entry used by `registerShortcutMap` and `useShortcutMap`. */
|
|
294
|
+
type ShortcutMapEntry = {
|
|
295
|
+
keys: string | string[];
|
|
296
|
+
handler: ShortcutHandler;
|
|
297
|
+
options?: HandlerOptions;
|
|
298
|
+
};
|
|
299
|
+
/** Bulk registration shape mapping action ids to key+handler definitions. */
|
|
300
|
+
type ShortcutMap = Record<string, ShortcutMapEntry>;
|
|
301
|
+
/** Return type for map registrations, keyed by the same ids as the source map. */
|
|
302
|
+
type ShortcutMapResult<T extends ShortcutMap = ShortcutMap> = {
|
|
303
|
+
[K in keyof T]: ShortcutResult;
|
|
304
|
+
};
|
|
305
|
+
/** Imperative grouping controller for binding/unbinding many shortcut registrations together. */
|
|
306
|
+
type ShortcutGroup = {
|
|
307
|
+
add: (...results: ShortcutResult[]) => void;
|
|
308
|
+
addMany: (results: ShortcutResult[] | Record<string, ShortcutResult>) => void;
|
|
309
|
+
unbindAll: () => void;
|
|
310
|
+
clear: () => void;
|
|
311
|
+
getResults: () => ShortcutResult[];
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
export type { ActionKey as A, ExceptPredicate as E, FunctionKey as F, HandlerOptions as H, KeyChain as K, ModifierChain as M, NavigationKey as N, ParsedShortcut as P, ShortcutAttemptDebugEvent as S, UseShortcutOptions as U, AlphaKey as a, ExceptPreset as b, ModifierFlags as c, ModifierName as d, ModifierState as e, NumericKey as f, ShortcutAttemptStatus as g, ShortcutBuilder as h, ShortcutConflict as i, ShortcutDebugEvent as j, ShortcutDebugInput as k, ShortcutDebugOptions as l, ShortcutDebugStep as m, ShortcutDebugToken as n, ShortcutDebugTokenStatus as o, ShortcutGroup as p, ShortcutHandler as q, ShortcutMap as r, ShortcutMapEntry as s, ShortcutMapResult as t, ShortcutRecordingOptions as u, ShortcutResult as v, ShortcutScope as w, SpecialKey as x, SymbolKey as y };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remcostoeten/use-shortcut",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.2.0",
|
|
4
|
+
"description": "Tiny, chainable React keyboard shortcuts with sequences, scopes, and typed debug hooks.",
|
|
5
5
|
"author": "Remco Stoeten",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"homepage": "https://github.com/remcostoeten/use-shortcut#readme",
|
|
@@ -23,9 +23,6 @@
|
|
|
23
23
|
"chainable"
|
|
24
24
|
],
|
|
25
25
|
"sideEffects": false,
|
|
26
|
-
"bin": {
|
|
27
|
-
"use-shortcut": "dist/cli/index.mjs"
|
|
28
|
-
},
|
|
29
26
|
"main": "./dist/index.js",
|
|
30
27
|
"module": "./dist/index.mjs",
|
|
31
28
|
"types": "./dist/index.d.ts",
|
|
@@ -39,6 +36,46 @@
|
|
|
39
36
|
"types": "./dist/index.d.ts",
|
|
40
37
|
"default": "./dist/index.js"
|
|
41
38
|
}
|
|
39
|
+
},
|
|
40
|
+
"./react": {
|
|
41
|
+
"import": {
|
|
42
|
+
"types": "./dist/react.d.ts",
|
|
43
|
+
"default": "./dist/react.mjs"
|
|
44
|
+
},
|
|
45
|
+
"require": {
|
|
46
|
+
"types": "./dist/react.d.ts",
|
|
47
|
+
"default": "./dist/react.js"
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"./parser": {
|
|
51
|
+
"import": {
|
|
52
|
+
"types": "./dist/parser.d.ts",
|
|
53
|
+
"default": "./dist/parser.mjs"
|
|
54
|
+
},
|
|
55
|
+
"require": {
|
|
56
|
+
"types": "./dist/parser.d.ts",
|
|
57
|
+
"default": "./dist/parser.js"
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"./formatter": {
|
|
61
|
+
"import": {
|
|
62
|
+
"types": "./dist/formatter.d.ts",
|
|
63
|
+
"default": "./dist/formatter.mjs"
|
|
64
|
+
},
|
|
65
|
+
"require": {
|
|
66
|
+
"types": "./dist/formatter.d.ts",
|
|
67
|
+
"default": "./dist/formatter.js"
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"./constants": {
|
|
71
|
+
"import": {
|
|
72
|
+
"types": "./dist/constants.d.ts",
|
|
73
|
+
"default": "./dist/constants.mjs"
|
|
74
|
+
},
|
|
75
|
+
"require": {
|
|
76
|
+
"types": "./dist/constants.d.ts",
|
|
77
|
+
"default": "./dist/constants.js"
|
|
78
|
+
}
|
|
42
79
|
}
|
|
43
80
|
},
|
|
44
81
|
"files": [
|
|
@@ -56,9 +93,13 @@
|
|
|
56
93
|
"build": "tsup && node scripts/prune-dist.mjs",
|
|
57
94
|
"dev": "tsup --watch",
|
|
58
95
|
"test": "vitest run --environment jsdom",
|
|
96
|
+
"test:watch": "vitest --environment jsdom",
|
|
97
|
+
"test:entrypoints": "vitest run --environment jsdom src/__tests__/entrypoints.test.ts",
|
|
98
|
+
"test:features": "vitest run --environment jsdom src/__tests__/features.test.ts",
|
|
59
99
|
"typecheck": "tsc --noEmit",
|
|
60
100
|
"docs:api": "node scripts/generate-api-reference.mjs",
|
|
61
101
|
"docs:check": "bun run docs:api && node scripts/docs-check.mjs",
|
|
102
|
+
"verify": "bun run typecheck && bun run test && bun run build && bun run docs:check",
|
|
62
103
|
"clean": "rm -rf dist",
|
|
63
104
|
"prepublishOnly": "npm run build"
|
|
64
105
|
},
|