@peter.naydenov/shortcuts 3.4.0 → 3.5.1

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.
Files changed (45) hide show
  1. package/Changelog.md +11 -0
  2. package/README.md +38 -9
  3. package/dist/main.d.ts +120 -0
  4. package/dist/methods/_normalizeWithPlugins.d.ts +2 -0
  5. package/dist/methods/_readShortcutWithPlugins.d.ts +2 -0
  6. package/dist/methods/_systemAction.d.ts +2 -0
  7. package/dist/methods/changeContext.d.ts +2 -0
  8. package/dist/methods/index.d.ts +17 -0
  9. package/dist/methods/listShortcuts.d.ts +17 -0
  10. package/dist/methods/load.d.ts +2 -0
  11. package/dist/methods/unload.d.ts +2 -0
  12. package/dist/plugins/click/_findTarget.d.ts +2 -0
  13. package/dist/plugins/click/_listenDOM.d.ts +5 -0
  14. package/dist/plugins/click/_normalizeShortcutName.d.ts +2 -0
  15. package/dist/plugins/click/_readClickEvent.d.ts +2 -0
  16. package/dist/plugins/click/_registerShortcutEvents.d.ts +2 -0
  17. package/dist/plugins/click/index.d.ts +15 -0
  18. package/dist/plugins/form/_defaults.d.ts +5 -0
  19. package/dist/plugins/form/_listenDOM.d.ts +5 -0
  20. package/dist/plugins/form/_normalizeShortcutName.d.ts +2 -0
  21. package/dist/plugins/form/_registerShortcutEvents.d.ts +2 -0
  22. package/dist/plugins/form/index.d.ts +10 -0
  23. package/dist/plugins/key/_listenDOM.d.ts +5 -0
  24. package/dist/plugins/key/_normalizeShortcutName.d.ts +2 -0
  25. package/dist/plugins/key/_readKeyEvent.d.ts +2 -0
  26. package/dist/plugins/key/_registerShortcutEvents.d.ts +2 -0
  27. package/dist/plugins/key/_specialChars.d.ts +32 -0
  28. package/dist/plugins/key/index.d.ts +15 -0
  29. package/dist/shortcuts.cjs +1 -1
  30. package/dist/shortcuts.esm.mjs +1 -1
  31. package/dist/shortcuts.umd.js +1 -1
  32. package/package.json +9 -5
  33. package/src/main.js +74 -20
  34. package/src/methods/changeContext.js +2 -1
  35. package/src/methods/load.js +4 -3
  36. package/src/methods/unload.js +3 -3
  37. package/src/plugins/click/_listenDOM.js +1 -1
  38. package/src/plugins/click/index.js +10 -0
  39. package/src/plugins/form/index.js +8 -13
  40. package/src/plugins/key/index.js +10 -0
  41. package/test/01-general.test.js +64 -5
  42. package/test/02-key.test.js +136 -38
  43. package/test/03-click.test.js +88 -56
  44. package/test/04-form.test.js +52 -9
  45. package/tsconfig.json +23 -0
@@ -1 +1 @@
1
- !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).shortcuts={})}(this,function(t){"use strict";var e={_normalizeWithPlugins:function(t,e){return function(t){const n=e.shortcuts;Object.keys(n).forEach(e=>{Object.entries(n[e]).forEach(([o,r])=>{const i=t(o);i!==o&&(delete n[e][o],n[e][i]=r)})})}},_readShortcutWithPlugins:function(t,e){return function(n){const{inAPI:o}=t,r=n.split(":")[0].toLowerCase().trim(),i=o._systemAction(r,"none");let s=n;return-1!==i&&(s=e.plugins[i].shortcutName(n)),s}},_systemAction:function(t,e){return function(t,n,o=null){return e.plugins.findIndex(e=>e.getPrefix()===t&&(e[n]&&e[n](o),!0))}},changeContext:function(t,e){const{shortcuts:n,currentContext:o}=e,{ev:r}=t;return function(t=!1){const i=o.name;if(!t)return r.reset(),void(o.name=null);i!==t&&(n[t]?(n[i]&&r.reset(),o.name=t,e.plugins.forEach(e=>e.contextChange(t)),Object.entries(n[t]).forEach(([t,e])=>{e.forEach(e=>r.on(t,e))}),r.on("*",(...t)=>{e.exposeShortcut&&e.exposeShortcut(...t)})):r.emit("@shortcuts-error",`Context '${t}' does not exist`))}},listShortcuts:function(t,e){const n=e.shortcuts;return function(t=null){if(null!=t){let e=n[t];return null==e?null:Object.entries(e).map(([t,e])=>t)}return Object.keys(n).map(t=>{let e={};return e.context=t,e.shortcuts=Object.entries(n[t]).map(([t,e])=>t),e})}},load:function(t,e){const{shortcuts:n,plugins:o}=e,{API:{changeContext:r,getContext:i}}=t;return function(t){const e=i(),s=o.map(t=>t.getPrefix().toUpperCase());let c=!1;Object.entries(t).forEach(([t,r])=>{t===e&&(c=!0),n[t]={},Object.entries(r).forEach(([e,r])=>{let i=e,c=e.toUpperCase().trim(),u=s.map((t,e)=>c.startsWith(t)?e:null).filter(t=>null!==t);if(u.length){let t=u[0];i=o[t].shortcutName(e)}r instanceof Function&&(r=[r]),n[t][i]=r})}),c&&(r(),r(e))}},unload:function(t,e){const{currentContext:n,shortcuts:o}=e,{ev:r}=t;return function(t){n.name!==t?o[t]?delete o[t]:r.emit("shortcuts-error",`Context '${t}' does not exist`):r.emit("shortcuts-error",`Context '${t}' can't be removed during is current active context. Change the context first`)}}};function n(t){const e=t.toUpperCase(),n=/KEY\s*\:/i.test(e),o=e.indexOf(":");if(!n)return t;return`KEY:${e.slice(o+1).split(",").map(t=>t.trim()).map(t=>t.split("+").map(t=>t.trim()).sort().join("+")).join(",")}`}function o(t,e){let{shiftKey:n,altKey:o,ctrlKey:r}=t,i=e(),s=t.code.replace("Key","").replace("Digit",""),c=[];return r&&c.push("CTRL"),n&&c.push("SHIFT"),o&&c.push("ALT"),i.hasOwnProperty(s)?c.push(i[s].toUpperCase()):["ControlLeft","ControlRight","ShiftLeft","ShiftRight","AltLeft","AltRight","Meta"].includes(s)||c.push(s.toUpperCase()),c.sort()}function r(t,e){let n=0;const{regex:o}=t,{listenOptions:r,currentContext:{name:i},shortcuts:s}=e;return null==i?0:(Object.entries(s[i]).forEach(([t,e])=>{if(!o.test(t))return;n++;let i=t.slice(4).split(",").length;r.maxSequence<i&&(r.maxSequence=i)}),n)}function i(){return{ArrowLeft:"LEFT",ArrowUp:"UP",ArrowRight:"RIGHT",ArrowDown:"DOWN",Enter:"ENTER",NumpadEnter:"ENTER",Escape:"ESC",Backspace:"BACKSPACE",Space:"SPACE",Tab:"TAB",Backquote:"`",BracketLeft:"[",BracketRight:"]",Equal:"=",Slash:"/",Backslash:"\\",IntlBackslash:"`",F1:"F1",F2:"F2",F3:"F3",F4:"F4",F5:"F5",F6:"F6",F7:"F7",F8:"F8",F9:"F9",F10:"F10",F11:"F11",F12:"F12"}}function s(t,e,n){const{listenOptions:{clickTarget:o}}=e;let r=n;return r===document||r===document.body?null:r.dataset[o]||"A"===r.nodeName?r:s(t,e,r.parentNode)}function c(t){const e=t.toUpperCase(),n=/CLICK\s*\:/i.test(e),o=["LEFT","MIDDLE","RIGHT"],r=["ALT","SHIFT","CTRL"];let i=null,s=[],c=0,u=e.indexOf(":");return n?(e.slice(u+1).trim().split("-").map(t=>t.trim()).forEach(t=>{o.includes(t)?i=t:r.includes(t)?s.push(t):isNaN(t)||(c=t)}),`CLICK:${i}-${c}${s.length>0?"-":""}${s.sort().join("-")}`):t}function u(t,e){let{shiftKey:n,altKey:o,ctrlKey:r,key:i,button:s}=t,c=`CLICK:${["LEFT","MIDDLE","RIGHT"][s]}-${e}`,u=[];return r&&u.push("CTRL"),n&&u.push("SHIFT"),o&&u.push("ALT"),u.length>0?`${c}${u.length>0?"-":""}${u.sort().join("-")}`:`${c}`}function l(t,e){let n=0;const{regex:o}=t,{listenOptions:r,currentContext:{name:i},shortcuts:s}=e;return null==i?0:(Object.entries(s[i]).forEach(([t,e])=>{if(!o.test(t))return;n++;let[,i]=t.slice(6).split("-");r.maxClicks<i&&(r.maxClicks=i)}),n)}function a(t){const e=t.toUpperCase(),n=/FORM\s*\:/i.test(e),o=e.indexOf(":");if(!n)return t;return`FORM:${e.slice(o+1).trim()}`}function f(t,e){const{regex:n,_defaults:o,ev:r}=t,{currentContext:{name:i},shortcuts:s,callbacks:c}=e;let u=[],l=[],a=[];if(null==i)return!1;if(Object.entries(s[i]).forEach(([t,e])=>{n.test(t)&&("FORM:WATCH"===t&&(u=e),"FORM:DEFINE"===t&&(l=e),"FORM:ACTION"===t&&(a=e))}),0===a.length)return!1;let f=new Set;0===l.length&&(l=[o.define]),0===u.length&&(u=[o.watch]);let m=u.map(t=>t()).reduce((t,e)=>(t.push(e),t),[]);return e.watchList=document.querySelectorAll(m),e.watchList.forEach(t=>f.add(l[0](t))),e.typeFn=l[0]?l[0]:o.define,a.forEach(t=>{if(!(t instanceof Function))return console.warn("Warning: The 'form:action' should be a function."),!1;if(!(t()instanceof Array))return console.warn("Warning: The 'form:action' function should RETURN an array."),!1;t().forEach(({fn:t,type:n,timing:o,wait:i=0})=>{if(f.has(n)&&t instanceof Function){let e=`${n}/${o}`;const i=c.hasOwnProperty(e);i?c[e].push(t):c[e]=[t],i||r.on(e,(t,e)=>{e.forEach(e=>{e instanceof Function&&e(t)})})}"instant"===o&&(e.wait[`${n}`]=i)})}),Object.keys(e.callbacks).length>0}const m={watch:()=>"input, select, textarea, button, a",define:t=>"checkbox"===t.type||"radio"===t.type?"checkbox":"button"==t.type||"submit"==t.type?"button":"input"};t.pluginClick=function(t,e,n){let{currentContext:o,shortcuts:r}=e,{inAPI:i}=t,a={ev:t.ev,_findTarget:s,_readClickEvent:u,mainDependencies:t,regex:/CLICK\s*\:/i},f={currentContext:o,active:!1,shortcuts:r,listenOptions:{mouseWait:n.mouseWait?n.mouseWait:320,maxClicks:1,clickTarget:n.clickTarget?n.clickTarget:"click"}};i._normalizeWithPlugins(c);let m=function(t,e){const{ev:n,_findTarget:o,_readClickEvent:r,mainDependencies:i}=t,{listenOptions:s,currentContext:c}=e,{mouseWait:u}=s;let l=null,a=null,f=null,m=null,p=0;function h(){const t=r(a,p),e={target:l,targetProps:l?l.getBoundingClientRect():null,x:a.clientX,y:a.clientY,context:c.name,note:c.note,event:a,dependencies:i.extra,type:"click"};n.emit(t,e),f=null,m=null,l=null,a=null,p=0}function d(n){let r=s.maxClicks;return clearTimeout(f),m?(clearTimeout(m),void(m=setTimeout(()=>m=null,u))):(l=o(t,e,n.target),l&&l.dataset.hasOwnProperty("quickClick")&&(r=1),l&&"A"===l.tagName&&(r=1),a=n,p++,p>=r?(h(),void(r>1&&(m=setTimeout(()=>m=null,u)))):void(f=setTimeout(h,u)))}function g(n){let r=s.maxClicks;return clearTimeout(f),m?(clearTimeout(m),void(m=setTimeout(()=>m=null,u))):(l=o(t,e,n.target),l&&l.dataset.hasOwnProperty("quickClick")&&(r=1),l&&"A"===l.tagName&&(r=1),a=n,p++,p>=r?(h(),void(r>1&&(m=setTimeout(()=>m=null,u)))):void(f=setTimeout(h,u)))}return{start:function(){e.active||(window.addEventListener("contextmenu",g),document.addEventListener("click",d),e.active=!0)},stop:function(){e.active&&(window.removeEventListener("contextmenu",g),document.removeEventListener("click",d),e.active=!1,f&&(clearTimeout(f),f=null),m&&(clearTimeout(m),m=null),p=0)}}}(a,f),p=l(a,f);p>0&&m.start();let h={getPrefix:()=>"click",shortcutName:t=>c(t),contextChange:()=>{p=l(a,f),p<1&&m.stop(),p>0&&m.start()},mute:()=>{m.stop()},unmute:()=>m.start(),destroy:()=>m.stop()};return Object.freeze(h),h},t.pluginForm=function(t,e,n){let{currentContext:o,shortcuts:r}=e,{inAPI:i}=t,s={ev:t.ev,mainDependencies:t,regex:/FORM\s*\:/i,_defaults:m},c={currentContext:o,shortcuts:r,callbacks:{},typeFn:"",watchList:[],wait:{}};i._normalizeWithPlugins(a);let u=function(t,e){const{ev:n}=t;let o=null;function r(t,e,n,o){return{target:n.target,context:e.currentContext.name,note:e.currentContext.note,event:n,dependencies:t.mainDependencies.extra,type:o}}function i(o){const{callbacks:i,typeFn:s}=e,c=s(o.target),u=r(t,e,o,c),l=`${c}/in`;null!=i[l]&&n.emit(l,u,i[l])}function s(o){const{callbacks:i,typeFn:s}=e,c=r(t,e,o,"form-out"),u=`${s(o.target)}/out`;null!=i[u]&&n.emit(u,c,i[u])}function c(i){const{callbacks:s,typeFn:c}=e,u=r(t,e,i,"form-instant"),l=c(i.target),a=e.wait[`${l}`],f=`${l}/instant`;null!=s[f]&&(0!==a?(clearTimeout(o),o=setTimeout(()=>n.emit(f,u,s[f]),a)):n.emit(f,u,s[f]))}return{start:function(){e.active||(document.addEventListener("focusin",i),document.addEventListener("focusout",s),document.addEventListener("input",c),e.active=!0)},stop:function(){e.active&&(document.removeEventListener("focusin",i),document.removeEventListener("focusout",s),document.removeEventListener("input",c),e.active=!1)}}}(s,c);if(o.name){f(s,c)&&u.start()}let l={getPrefix:()=>"form",shortcutName:t=>a(t),contextChange:()=>{c.callbacks={},c.typeFn="",c.watchList=[],c.wait={},f(s,c)?u.start():u.stop()},mute:()=>u.stop(),unmute:()=>u.start(),destroy:()=>{u.stop()}};return Object.freeze(l),l},t.pluginKey=function(t,e,s={}){let{currentContext:c,shortcuts:u,exposeShortcut:l}=e,{inAPI:a}=t,f={ev:t.ev,_specialChars:i,_readKeyEvent:o,mainDependencies:t,regex:/KEY\s*\:/i},m={currentContext:c,shortcuts:u,active:!1,listenOptions:{keyWait:s.keyWait?s.keyWait:480,maxSequence:1,keyIgnore:null},streamKeys:!(!s.streamKeys||"function"!=typeof s.streamKeys)&&s.streamKeys};a._normalizeWithPlugins(n);let p=function(t,e){const{ev:n,_specialChars:o,_readKeyEvent:r,mainDependencies:i}=t,{currentContext:s,streamKeys:c,listenOptions:u}=e,{keyWait:l}=u;let a=[],f=null,m=!0,p=!1;const h=()=>m=!1,d=()=>m=!0,g=()=>p=!0,y=()=>!1===m;function x(){let t=a.map(t=>[t.join("+")]);const e={wait:h,end:d,ignore:g,isWaiting:y,note:s.note,context:s.name,dependencies:i.extra,type:"key"};if(!m){let o=t.at(-1);n.emit(o,e),p&&(t=t.slice(0,-1),p=!1)}if(m){const o=`KEY:${t.join(",")}`;n.emit(o,e),a=[],f=null}}function C(e){if(clearTimeout(f),o().hasOwnProperty(e.code))return a.push(r(e,o)),c&&c({key:e.key,context:s.name,note:s.note,dependencies:t.extra}),u.keyIgnore?(clearTimeout(u.keyIgnore),void(u.keyIgnore=setTimeout(()=>u.keyIgnore=null,l))):m&&a.length===u.maxSequence?(x(),void(u.keyIgnore=setTimeout(()=>u.keyIgnore=null,l))):void(m?f=setTimeout(x,l):x())}function k(e){if(!o().hasOwnProperty(e.code)){if(clearTimeout(f),c&&c({key:e.key,context:s.name,note:s.note,dependencies:t.extra}),u.keyIgnore)return clearTimeout(u.keyIgnore),void(u.keyIgnore=setTimeout(()=>u.keyIgnore=null,l));if(a.push(r(e,o)),m&&a.length===u.maxSequence)return x(),void(u.keyIgnore=setTimeout(()=>u.keyIgnore=null,l));m?f=setTimeout(x,l):x()}}return{start:function(){e.active||(document.addEventListener("keydown",C),document.addEventListener("keypress",k),e.active=!0)},stop:function(){e.active&&(document.removeEventListener("keydown",C),document.removeEventListener("keypress",k),e.active=!1,f&&(clearTimeout(f),f=null),u.keyIgnore&&(clearTimeout(u.keyIgnore),u.keyIgnore=null),a=[],m=!0,p=!1)}}}(f,m),h=r(f,m);h>0&&p.start();let d={getPrefix:()=>"key",shortcutName:t=>n(t),contextChange:t=>{h=r(f,m),h<1&&p.stop(),h>0&&p.start()},mute:()=>p.stop(),unmute:()=>p.start(),destroy:()=>p.stop()};return Object.freeze(d),d},t.shortcuts=function(t={}){let n={},o={};const r=function(){let t={"*":[]},e={},n=new Set,o=!1,r="";return{on:function(e,n){t[e]||(t[e]=[]),t[e].push(n)},once:function(t,n){"*"!==t&&(e[t]||(e[t]=[]),e[t].push(n))},off:function(n,o){if(o)return t[n]&&(t[n]=t[n].filter(t=>t!==o)),e[n]&&(e[n]=e[n].filter(t=>t!==o)),t[n]&&0===t[n].length&&delete t[n],void(e[n]&&0===e[n].length&&delete t[n]);e[n]&&delete e[n],t[n]&&delete t[n]},reset:function(){t={"*":[]},e={},n=new Set},emit:function(){const[i,...s]=arguments;function c(e){let o=!1;"*"!==e&&(n.has(e)||(t[e].every(t=>{const e=t(...s);return"string"!=typeof e||"STOP"!==e.toUpperCase()||(o=!0,!1)}),o||t["*"].forEach(t=>t(i,...s))))}if(o&&(console.log(`${r} Event "${i}" was triggered.`),s.length>0&&(console.log("Arguments:"),console.log(...s),console.log("^----"))),"*"!==i){if(e[i]){if(n.has(i))return;e[i].forEach(t=>t(...s)),delete e[i]}t[i]&&c(i)}else Object.keys(t).forEach(t=>c(t))},stop:function(o){if("*"===o){const o=Object.keys(t),r=Object.keys(e);return void(n=new Set([...r,...o]))}n.add(o)},start:function(t){"*"!==t?n.delete(t):n.clear()},debug:function(t,e){o=!!t,e&&"string"==typeof e&&(r=e)}}}(),i={currentContext:{name:null,note:null},shortcuts:{},plugins:[],exposeShortcut:!(!t.onShortcut||"function"!=typeof t.onShortcut)&&t.onShortcut};let s={ev:r,inAPI:n,API:o,extra:{}};return o.enablePlugin=(t,e={})=>{const o=t.name.replace("plugin","").toLowerCase();if(-1===n._systemAction(o,"none")){let n=t(s,i,e);i.plugins.push(n)}},o.disablePlugin=t=>{const e=n._systemAction(t,"destroy");-1!==e&&(i.plugins=i.plugins.filter((t,n)=>n!==e))},o.mutePlugin=t=>n._systemAction(t,"mute"),o.unmutePlugin=t=>n._systemAction(t,"unmute"),o.getContext=()=>i.currentContext.name,o.getNote=()=>i.currentContext.note,o.setNote=(t=null)=>{"string"!=typeof t&&null!=t||(i.currentContext.note=t)},o.pause=(t="*")=>{let e=n._readShortcutWithPlugins(t);r.stop(e)},o.resume=(t="*")=>{const e=n._readShortcutWithPlugins(t);r.start(e)},o.emit=(t,...e)=>r.emit(n._readShortcutWithPlugins(t),...e),o.listContexts=()=>Object.keys(i.shortcuts),o.setDependencies=t=>s.extra={...s.extra,...t},o.getDependencies=()=>s.extra,o.reset=function(){r.reset(),o.changeContext(),i.plugins.forEach(t=>t.destroy()),o.listContexts().map(t=>o.unload(t)),s.extra={},i.exposeShortcut=null},Object.entries(e).forEach(([t,e])=>{t.startsWith("_")?n[t]=e(s,i):o[t]=e(s,i)}),o}});
1
+ !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).shortcuts={})}(this,function(e){"use strict";var t={_normalizeWithPlugins:function(e,t){return function(e){const n=t.shortcuts;Object.keys(n).forEach(t=>{Object.entries(n[t]).forEach(([o,r])=>{const i=e(o);i!==o&&(delete n[t][o],n[t][i]=r)})})}},_readShortcutWithPlugins:function(e,t){return function(n){const{inAPI:o}=e,r=n.split(":")[0].toLowerCase().trim(),i=o._systemAction(r,"none");let s=n;return-1!==i&&(s=t.plugins[i].shortcutName(n)),s}},_systemAction:function(e,t){return function(e,n,o=null){return t.plugins.findIndex(t=>t.getPrefix()===e&&(t[n]&&t[n](o),!0))}},changeContext:function(e,t){const{shortcuts:n,currentContext:o,ERROR_EVENT_NAME:r}=t,{ev:i}=e;return function(e=!1){const s=o.name;if(!e)return i.reset(),void(o.name=null);s!==e&&(n[e]?(n[s]&&i.reset(),o.name=e,t.plugins.forEach(t=>t.contextChange(e)),Object.entries(n[e]).forEach(([e,t])=>{t.forEach(t=>i.on(e,t))}),i.on("*",(...e)=>{t.exposeShortcut&&t.exposeShortcut(...e)})):i.emit(r,`Context '${e}' does not exist`))}},listShortcuts:function(e,t){const n=t.shortcuts;return function(e=null){if(null!=e){let t=n[e];return null==t?null:Object.entries(t).map(([e,t])=>e)}return Object.keys(n).map(e=>{let t={};return t.context=e,t.shortcuts=Object.entries(n[e]).map(([e,t])=>e),t})}},load:function(e,t){const{shortcuts:n,plugins:o}=t,{API:{changeContext:r,getContext:i}}=e;return function(e){const t=i(),s=o.map(e=>e.getPrefix().toUpperCase());let c=!1;Object.entries(e).forEach(([e,r])=>{e===t&&(c=!0),n[e]={},Object.entries(r).forEach(([t,r])=>{let i=t,c=t.toUpperCase().trim(),u=s.map((e,t)=>c.startsWith(e)?t:null).filter(e=>null!==e);if(u.length){let e=u[0];i=o[e].shortcutName(t)}r instanceof Function&&(r=[r]),n[e][i]=r})}),c&&(r(),r(t))}},unload:function(e,t){const{currentContext:n,shortcuts:o,ERROR_EVENT_NAME:r}=t,{ev:i}=e;return function(e){n.name!==e?o[e]?delete o[e]:i.emit(r,`Context '${e}' does not exist`):i.emit(r,`Context '${e}' can't be removed during is current active context. Change the context first`)}}};function n(e){const t=e.toUpperCase(),n=/KEY\s*\:/i.test(t),o=t.indexOf(":");if(!n)return e;return`KEY:${t.slice(o+1).split(",").map(e=>e.trim()).map(e=>e.split("+").map(e=>e.trim()).sort().join("+")).join(",")}`}function o(e,t){let{shiftKey:n,altKey:o,ctrlKey:r}=e,i=t(),s=e.code.replace("Key","").replace("Digit",""),c=[];return r&&c.push("CTRL"),n&&c.push("SHIFT"),o&&c.push("ALT"),i.hasOwnProperty(s)?c.push(i[s].toUpperCase()):["ControlLeft","ControlRight","ShiftLeft","ShiftRight","AltLeft","AltRight","Meta"].includes(s)||c.push(s.toUpperCase()),c.sort()}function r(e,t){let n=0;const{regex:o}=e,{listenOptions:r,currentContext:{name:i},shortcuts:s}=t;return null==i?0:(Object.entries(s[i]).forEach(([e,t])=>{if(!o.test(e))return;n++;let i=e.slice(4).split(",").length;r.maxSequence<i&&(r.maxSequence=i)}),n)}function i(){return{ArrowLeft:"LEFT",ArrowUp:"UP",ArrowRight:"RIGHT",ArrowDown:"DOWN",Enter:"ENTER",NumpadEnter:"ENTER",Escape:"ESC",Backspace:"BACKSPACE",Space:"SPACE",Tab:"TAB",Backquote:"`",BracketLeft:"[",BracketRight:"]",Equal:"=",Slash:"/",Backslash:"\\",IntlBackslash:"`",F1:"F1",F2:"F2",F3:"F3",F4:"F4",F5:"F5",F6:"F6",F7:"F7",F8:"F8",F9:"F9",F10:"F10",F11:"F11",F12:"F12"}}function s(e,t,n){const{listenOptions:{clickTarget:o}}=t;let r=n;return r===document||r===document.body?null:r.dataset[o]||"A"===r.nodeName?r:s(e,t,r.parentNode)}function c(e){const t=e.toUpperCase(),n=/CLICK\s*\:/i.test(t),o=["LEFT","MIDDLE","RIGHT"],r=["ALT","SHIFT","CTRL"];let i=null,s=[],c=0,u=t.indexOf(":");return n?(t.slice(u+1).trim().split("-").map(e=>e.trim()).forEach(e=>{o.includes(e)?i=e:r.includes(e)?s.push(e):isNaN(e)||(c=e)}),`CLICK:${i}-${c}${s.length>0?"-":""}${s.sort().join("-")}`):e}function u(e,t){let{shiftKey:n,altKey:o,ctrlKey:r,key:i,button:s}=e,c=`CLICK:${["LEFT","MIDDLE","RIGHT"][s]}-${t}`,u=[];return r&&u.push("CTRL"),n&&u.push("SHIFT"),o&&u.push("ALT"),u.length>0?`${c}${u.length>0?"-":""}${u.sort().join("-")}`:`${c}`}function l(e,t){let n=0;const{regex:o}=e,{listenOptions:r,currentContext:{name:i},shortcuts:s}=t;return null==i?0:(Object.entries(s[i]).forEach(([e,t])=>{if(!o.test(e))return;n++;let[,i]=e.slice(6).split("-");r.maxClicks<i&&(r.maxClicks=i)}),n)}function a(e){const t=e.toUpperCase(),n=/FORM\s*\:/i.test(t),o=t.indexOf(":");if(!n)return e;return`FORM:${t.slice(o+1).trim()}`}function f(e,t){const{regex:n,_defaults:o,ev:r}=e,{currentContext:{name:i},shortcuts:s,callbacks:c}=t;let u=[],l=[],a=[];if(null==i)return!1;if(Object.entries(s[i]).forEach(([e,t])=>{n.test(e)&&("FORM:WATCH"===e&&(u=t),"FORM:DEFINE"===e&&(l=t),"FORM:ACTION"===e&&(a=t))}),0===a.length)return!1;let f=new Set;0===l.length&&(l=[o.define]),0===u.length&&(u=[o.watch]);let m=u.map(e=>e()).reduce((e,t)=>(e.push(t),e),[]);return t.watchList=document.querySelectorAll(m),t.watchList.forEach(e=>f.add(l[0](e))),t.typeFn=l[0]?l[0]:o.define,a.forEach(e=>{if(!(e instanceof Function))return console.warn("Warning: The 'form:action' should be a function."),!1;if(!(e()instanceof Array))return console.warn("Warning: The 'form:action' function should RETURN an array."),!1;e().forEach(({fn:e,type:n,timing:o,wait:i=0})=>{if(f.has(n)&&e instanceof Function){let t=`${n}/${o}`;const i=c.hasOwnProperty(t);i?c[t].push(e):c[t]=[e],i||r.on(t,(e,t)=>{t.forEach(t=>{t instanceof Function&&t(e)})})}"instant"===o&&(t.wait[`${n}`]=i)})}),Object.keys(t.callbacks).length>0}const m={watch:()=>"input, select, textarea, button, a",define:e=>"checkbox"===e.type||"radio"===e.type?"checkbox":"button"==e.type||"submit"==e.type?"button":"input"};e.pluginClick=function(e,t,n){let{currentContext:o,shortcuts:r}=t,{inAPI:i}=e,a={ev:e.ev,_findTarget:s,_readClickEvent:u,mainDependencies:e,regex:/CLICK\s*\:/i},f={currentContext:o,active:!1,shortcuts:r,listenOptions:{mouseWait:n.mouseWait?n.mouseWait:320,maxClicks:1,clickTarget:n.clickTarget?n.clickTarget:"click"}};i._normalizeWithPlugins(c);let m=function(e,t){const{ev:n,_findTarget:o,_readClickEvent:r,mainDependencies:i}=e,{listenOptions:s,currentContext:c}=t,{mouseWait:u}=s;let l=null,a=null,f=null,m=null,p=0;function h(){const e=r(a,p),t={target:l,targetProps:l?l.getBoundingClientRect():null,x:a.clientX,y:a.clientY,context:c.name,note:c.note,event:a,dependencies:i.extra,type:"click"};n.emit(e,t),f=null,m=null,l=null,a=null,p=0}function d(n){let r=s.maxClicks;return clearTimeout(f),m?(clearTimeout(m),void(m=setTimeout(()=>m=null,u))):(l=o(e,t,n.target),l&&l.dataset.hasOwnProperty("quickClick")&&(r=1),l&&"A"===l.tagName&&(r=1),a=n,p++,p>=r?(h(),void(r>1&&(m=setTimeout(()=>m=null,u)))):void(f=setTimeout(h,u)))}function g(n){let r=s.maxClicks;return clearTimeout(f),m?(clearTimeout(m),void(m=setTimeout(()=>m=null,u))):(l=o(e,t,n.target),l&&l.dataset.hasOwnProperty("quickClick")&&(r=1),l&&"A"===l.tagName&&(r=1),a=n,p++,p>=r?(h(),void(r>1&&(m=setTimeout(()=>m=null,u)))):void(f=setTimeout(h,u)))}return{start:function(){t.active||(window.addEventListener("contextmenu",g),document.addEventListener("click",d),t.active=!0)},stop:function(){t.active&&(window.removeEventListener("contextmenu",g),document.removeEventListener("click",d),t.active=!1,f&&(clearTimeout(f),f=null),m&&(clearTimeout(m),m=null),p=0)}}}(a,f),p=l(a,f);p>0&&m.start();let h={getPrefix:()=>"click",shortcutName:e=>c(e),contextChange:()=>{p=l(a,f),p<1&&m.stop(),p>0&&m.start()},mute:()=>{m.stop()},unmute:()=>m.start(),destroy:()=>m.stop()};return Object.freeze(h),h},e.pluginForm=function(e,t,n){let{currentContext:o,shortcuts:r}=t,{inAPI:i}=e,s={ev:e.ev,mainDependencies:e,regex:/FORM\s*\:/i,_defaults:m},c={currentContext:o,shortcuts:r,callbacks:{},typeFn:"",watchList:[],wait:{}};i._normalizeWithPlugins(a);let u=function(e,t){const{ev:n}=e;let o=null;function r(e,t,n,o){return{target:n.target,context:t.currentContext.name,note:t.currentContext.note,event:n,dependencies:e.mainDependencies.extra,type:o}}function i(o){const{callbacks:i,typeFn:s}=t,c=s(o.target),u=r(e,t,o,c),l=`${c}/in`;null!=i[l]&&n.emit(l,u,i[l])}function s(o){const{callbacks:i,typeFn:s}=t,c=r(e,t,o,"form-out"),u=`${s(o.target)}/out`;null!=i[u]&&n.emit(u,c,i[u])}function c(i){const{callbacks:s,typeFn:c}=t,u=r(e,t,i,"form-instant"),l=c(i.target),a=t.wait[`${l}`],f=`${l}/instant`;null!=s[f]&&(0!==a?(clearTimeout(o),o=setTimeout(()=>n.emit(f,u,s[f]),a)):n.emit(f,u,s[f]))}return{start:function(){t.active||(document.addEventListener("focusin",i),document.addEventListener("focusout",s),document.addEventListener("input",c),t.active=!0)},stop:function(){t.active&&(document.removeEventListener("focusin",i),document.removeEventListener("focusout",s),document.removeEventListener("input",c),t.active=!1)}}}(s,c);if(o.name){f(s,c)&&u.start()}let l={getPrefix:()=>"form",shortcutName:e=>a(e),contextChange:()=>{c.callbacks={},c.typeFn="",c.watchList=[],c.wait={},f(s,c)?u.start():u.stop()},mute:()=>u.stop(),unmute:()=>u.start(),destroy:()=>{u.stop()}};return Object.freeze(l),l},e.pluginKey=function(e,t,s={}){let{currentContext:c,shortcuts:u,exposeShortcut:l}=t,{inAPI:a}=e,f={ev:e.ev,_specialChars:i,_readKeyEvent:o,mainDependencies:e,regex:/KEY\s*\:/i},m={currentContext:c,shortcuts:u,active:!1,listenOptions:{keyWait:s.keyWait?s.keyWait:480,maxSequence:1,keyIgnore:null},streamKeys:!(!s.streamKeys||"function"!=typeof s.streamKeys)&&s.streamKeys};a._normalizeWithPlugins(n);let p=function(e,t){const{ev:n,_specialChars:o,_readKeyEvent:r,mainDependencies:i}=e,{currentContext:s,streamKeys:c,listenOptions:u}=t,{keyWait:l}=u;let a=[],f=null,m=!0,p=!1;const h=()=>m=!1,d=()=>m=!0,g=()=>p=!0,y=()=>!1===m;function x(){let e=a.map(e=>[e.join("+")]);const t={wait:h,end:d,ignore:g,isWaiting:y,note:s.note,context:s.name,dependencies:i.extra,type:"key"};if(!m){let o=e.at(-1);n.emit(o,t),p&&(e=e.slice(0,-1),p=!1)}if(m){const o=`KEY:${e.join(",")}`;n.emit(o,t),a=[],f=null}}function E(t){if(clearTimeout(f),o().hasOwnProperty(t.code))return a.push(r(t,o)),c&&c({key:t.key,context:s.name,note:s.note,dependencies:e.extra}),u.keyIgnore?(clearTimeout(u.keyIgnore),void(u.keyIgnore=setTimeout(()=>u.keyIgnore=null,l))):m&&a.length===u.maxSequence?(x(),void(u.keyIgnore=setTimeout(()=>u.keyIgnore=null,l))):void(m?f=setTimeout(x,l):x())}function v(t){if(!o().hasOwnProperty(t.code)){if(clearTimeout(f),c&&c({key:t.key,context:s.name,note:s.note,dependencies:e.extra}),u.keyIgnore)return clearTimeout(u.keyIgnore),void(u.keyIgnore=setTimeout(()=>u.keyIgnore=null,l));if(a.push(r(t,o)),m&&a.length===u.maxSequence)return x(),void(u.keyIgnore=setTimeout(()=>u.keyIgnore=null,l));m?f=setTimeout(x,l):x()}}return{start:function(){t.active||(document.addEventListener("keydown",E),document.addEventListener("keypress",v),t.active=!0)},stop:function(){t.active&&(document.removeEventListener("keydown",E),document.removeEventListener("keypress",v),t.active=!1,f&&(clearTimeout(f),f=null),u.keyIgnore&&(clearTimeout(u.keyIgnore),u.keyIgnore=null),a=[],m=!0,p=!1)}}}(f,m),h=r(f,m);h>0&&p.start();let d={getPrefix:()=>"key",shortcutName:e=>n(e),contextChange:e=>{h=r(f,m),h<1&&p.stop(),h>0&&p.start()},mute:()=>p.stop(),unmute:()=>p.start(),destroy:()=>p.stop()};return Object.freeze(d),d},e.shortcuts=function(e={}){let n={},o={};const r=function(){let e={"*":[]},t={},n=new Set,o=!1,r="";return{on:function(t,n){e[t]||(e[t]=[]),e[t].push(n)},once:function(e,n){"*"!==e&&(t[e]||(t[e]=[]),t[e].push(n))},off:function(n,o){if(o)return e[n]&&(e[n]=e[n].filter(e=>e!==o)),t[n]&&(t[n]=t[n].filter(e=>e!==o)),e[n]&&0===e[n].length&&delete e[n],void(t[n]&&0===t[n].length&&delete e[n]);t[n]&&delete t[n],e[n]&&delete e[n]},reset:function(){e={"*":[]},t={},n=new Set},emit:function(){const[i,...s]=arguments;function c(t){let o=!1;"*"!==t&&(n.has(t)||(e[t].every(e=>{const t=e(...s);return"string"!=typeof t||"STOP"!==t.toUpperCase()||(o=!0,!1)}),o||e["*"].forEach(e=>e(i,...s))))}if(o&&(console.log(`${r} Event "${i}" was triggered.`),s.length>0&&(console.log("Arguments:"),console.log(...s),console.log("^----"))),"*"!==i){if(t[i]){if(n.has(i))return;t[i].forEach(e=>e(...s)),delete t[i]}e[i]&&c(i)}else Object.keys(e).forEach(e=>c(e))},stop:function(o){if("*"===o){const o=Object.keys(e),r=Object.keys(t);return void(n=new Set([...r,...o]))}n.add(o)},start:function(e){"*"!==e?n.delete(e):n.clear()},debug:function(e,t){o=!!e,t&&"string"==typeof t&&(r=t)}}}(),i={currentContext:{name:null,note:null},shortcuts:{},plugins:[],exposeShortcut:!(!e.onShortcut||"function"!=typeof e.onShortcut)&&e.onShortcut,ERROR_EVENT_NAME:e.errorEventName?e.errorEventName:"@shortcuts-error"};let s={ev:r,inAPI:n,API:o,extra:{}};return o.enablePlugin=(e,t={})=>{if("function"!=typeof e)return;let o=e(s,i,t);const r=o.getPrefix();-1===n._systemAction(r,"none")?i.plugins.push(o):o.destroy()},o.disablePlugin=e=>{const t=n._systemAction(e,"destroy");-1!==t&&(i.plugins=i.plugins.filter((e,n)=>n!==t))},o.mutePlugin=e=>n._systemAction(e,"mute"),o.unmutePlugin=e=>n._systemAction(e,"unmute"),o.listPlugins=()=>i.plugins.map(e=>e.getPrefix()),o.getContext=()=>i.currentContext.name,o.getNote=()=>i.currentContext.note,o.setNote=(e=null)=>{"string"!=typeof e&&null!=e||(i.currentContext.note=e)},o.pause=(e="*")=>{let t=n._readShortcutWithPlugins(e);r.stop(t)},o.resume=(e="*")=>{const t=n._readShortcutWithPlugins(e);r.start(t)},o.emit=(e,...t)=>r.emit(n._readShortcutWithPlugins(e),...t),o.listContexts=()=>Object.keys(i.shortcuts),o.setDependencies=e=>s.extra={...s.extra,...e},o.getDependencies=()=>s.extra,o.reset=function(){r.reset(),o.changeContext(),i.plugins.forEach(e=>e.destroy()),o.listContexts().map(e=>o.unload(e)),s.extra={},i.exposeShortcut=null},Object.entries(t).forEach(([e,t])=>{e.startsWith("_")?n[e]=t(s,i):o[e]=t(s,i)}),o}});
package/package.json CHANGED
@@ -1,13 +1,15 @@
1
1
  {
2
2
  "name": "@peter.naydenov/shortcuts",
3
3
  "description": "Context control of shortcuts based on keyboard and mouse events",
4
- "version": "3.4.0",
4
+ "version": "3.5.1",
5
5
  "license": "MIT",
6
6
  "author": "Peter Naydenov",
7
7
  "main": "./dist/shortcuts.umd.js",
8
+ "types": "./dist/main.d.ts",
8
9
  "type": "module",
9
10
  "exports": {
10
11
  ".": {
12
+ "types": "./dist/main.d.ts",
11
13
  "import": "./dist/shortcuts.esm.mjs",
12
14
  "require": "./dist/shortcuts.cjs",
13
15
  "default": "./dist/shortcuts.umd.js"
@@ -18,10 +20,11 @@
18
20
  },
19
21
  "scripts": {
20
22
  "dev": "vite",
21
- "build": "rollup -c",
23
+ "build": "rollup -c && npm run types",
22
24
  "test": "vitest",
23
- "test:ui": "vitest --ui",
24
- "cover": "vitest run --coverage"
25
+ "test:ui": "vitest --ui --open",
26
+ "cover": "vitest run --coverage",
27
+ "types": "tsc"
25
28
  },
26
29
  "repository": {
27
30
  "type": "git",
@@ -46,7 +49,8 @@
46
49
  "playwright": "^1.56.1",
47
50
  "react": "^19.2.0",
48
51
  "react-dom": "^19.2.0",
49
- "vite": "^7.1.10",
52
+ "typescript": "^5.9.3",
53
+ "vite": "^7.1.11",
50
54
  "vitest": "^3.2.4"
51
55
  },
52
56
  "keywords": [
package/src/main.js CHANGED
@@ -1,19 +1,52 @@
1
1
  'use strict'
2
2
 
3
3
  /**
4
- * Shortcuts
5
- * ========
6
- *
7
- * Create shortcuts for your web application based on keyboard and mouse and DOM events.
8
- * Repository: https://github.com/PeterNaydenov/shortcuts
9
- *
10
- * History notes:
11
- * - Development was started on June 21st, 2023
12
- * - First version was published on August 14th, 2023
13
- * - Method 'emit' was added on September 30st, 2023
14
- * - Version 2.0.0 was published on October 16th, 2023
15
- * - Version 3.0.0. Plugin system. Published on March 5th, 2024
16
- * - Version 3.2.0. Added plugin 'form'. Published on August 15th, 2025
4
+ * @typedef {Object} PluginAPI
5
+ * @property {function(): string} getPrefix - Get plugin prefix
6
+ * @property {function(string): string} shortcutName - Format shortcut name
7
+ * @property {function(string): void} contextChange - Handle context change
8
+ * @property {function(): void} mute - Mute the plugin
9
+ * @property {function(): void} unmute - Unmute the plugin
10
+ * @property {function(): void} destroy - Destroy the plugin
11
+ */
12
+
13
+ /**
14
+ * @typedef {Object} ShortcutsAPI
15
+ * @property {function(function, Object): void} enablePlugin - Enable a plugin
16
+ * @property {function(string): void} disablePlugin - Disable a plugin
17
+ * @property {function(string): number} mutePlugin - Mute a plugin
18
+ * @property {function(string): number} unmutePlugin - Unmute a plugin
19
+ * @property {function(): string[]} listPlugins - List enabled plugins
20
+ * @property {function(): string|null} getContext - Get current context name
21
+ * @property {function(): string|null} getNote - Get current context note
22
+ * @property {function(string|null): void} setNote - Set current context note
23
+ * @property {function(string): void} pause - Pause shortcuts in current context
24
+ * @property {function(string): void} resume - Resume shortcuts in current context
25
+ * @property {function(string, ...any): void} emit - Emit event for shortcut
26
+ * @property {function(): string[]} listContexts - List all context names
27
+ * @property {function(Object): void} setDependencies - Set external dependencies
28
+ * @property {function(): Object} getDependencies - Get external dependencies
29
+ * @property {function(): void} reset - Reset the library instance
30
+ * @property {function(string|boolean): void} changeContext - Change current context
31
+ * @property {function(string|null): string[]|Object[]} listShortcuts - List shortcuts
32
+ * @property {function(Object): void} load - Load shortcuts into contexts
33
+ * @property {function(string): void} unload - Unload a context
34
+ */
35
+
36
+ /**
37
+ * Shortcuts
38
+ * =========
39
+ *
40
+ * Create shortcuts for your web application based on keyboard and mouse and DOM events.
41
+ * Repository: https://github.com/PeterNaydenov/shortcuts
42
+ *
43
+ * History notes:
44
+ * - Development was started on June 21st, 2023
45
+ * - First version was published on August 14th, 2023
46
+ * - Method 'emit' was added on September 30st, 2023
47
+ * - Version 2.0.0 was published on October 16th, 2023
48
+ * - Version 3.0.0. Plugin system. Published on March 5th, 2024
49
+ * - Version 3.2.0. Added plugin 'form'. Published on August 15th, 2025
17
50
  */
18
51
 
19
52
 
@@ -30,6 +63,14 @@ import pluginForm from './plugins/form/index.js'
30
63
 
31
64
 
32
65
 
66
+ /**
67
+ * @function shortcuts
68
+ * @description Create a shortcuts instance
69
+ * @param {Object} [options={}] - Configuration options
70
+ * @param {function} [options.onShortcut] - Function to log shortcut events
71
+ * @param {string} [options.errorEventName='@shortcuts-error'] - Name for error events
72
+ * @returns {ShortcutsAPI} The shortcuts API
73
+ */
33
74
  function main ( options = {} ) {
34
75
  let
35
76
  inAPI = {} // API for internal methods
@@ -38,10 +79,11 @@ function main ( options = {} ) {
38
79
  const
39
80
  ev = notice () // Event emitter instance
40
81
  , state = {
41
- currentContext : { name: null, note: null } // Context data container
42
- , shortcuts : {} // shortcuts = { contextName : { shortcut : callback[] } }
43
- , plugins : [] // Array of active plugins
44
- , exposeShortcut : (options.onShortcut && ( typeof options.onShortcut === 'function')) ? options.onShortcut : false // Keyboard shortcut log function
82
+ currentContext : { name: null, note: null } // Context data container
83
+ , shortcuts : {} // shortcuts = { contextName : { shortcut : callback[] } }
84
+ , plugins : [] // Array of active plugins
85
+ , exposeShortcut : (options.onShortcut && ( typeof options.onShortcut === 'function')) ? options.onShortcut : false // Keyboard shortcut log function
86
+ , ERROR_EVENT_NAME : ( options.errorEventName ) ? options.errorEventName : '@shortcuts-error'
45
87
  } // state
46
88
  ;
47
89
  let dependencies = {
@@ -60,16 +102,20 @@ function main ( options = {} ) {
60
102
  * @returns {void}
61
103
  */
62
104
  API.enablePlugin = ( plugin, options={}) => {
105
+ if ( typeof plugin !== 'function' ) return
106
+ let plugApp = plugin ( dependencies, state, options )
63
107
  const
64
- name = plugin.name.replace ( 'plugin', '' ).toLowerCase ()
108
+ name = plugApp.getPrefix ()
65
109
  , ix = inAPI._systemAction ( name, 'none' )
66
110
  ;
67
111
 
68
112
  if ( ix === -1 ) { // If plugin is not registered
69
113
  // Started instance of the plugin
70
- let plugApp = plugin ( dependencies, state, options )
71
114
  state.plugins.push ( plugApp )
72
115
  }
116
+ else {
117
+ plugApp.destroy ()
118
+ }
73
119
  } // enable func.
74
120
 
75
121
 
@@ -100,8 +146,15 @@ function main ( options = {} ) {
100
146
  API.unmutePlugin = pluginName => inAPI._systemAction ( pluginName, 'unmute' )
101
147
 
102
148
 
103
-
149
+ /**
150
+ * @function listPlugins
151
+ * @description List all enabled plugins
152
+ * @returns {string[]} - Array of plugin names
153
+ */
154
+ API.listPlugins = () => state.plugins.map ( plugin => plugin.getPrefix() )
155
+
104
156
 
157
+
105
158
  // ---------------------- > PUBLIC METHODS < ---------------------- //
106
159
  /**
107
160
  * @function getContext
@@ -208,6 +261,7 @@ function main ( options = {} ) {
208
261
 
209
262
 
210
263
 
264
+ /** @type {function} */
211
265
  export { main as shortcuts }
212
266
  export {
213
267
  pluginKey
@@ -5,6 +5,7 @@ const
5
5
  {
6
6
  shortcuts
7
7
  , currentContext
8
+ , ERROR_EVENT_NAME
8
9
  } = state
9
10
  , { ev } = dependencies
10
11
  ;
@@ -37,7 +38,7 @@ return function changeContext ( contextName = false ) {
37
38
  }
38
39
  if ( current === contextName ) return // Do nothing if contextName is the same as current
39
40
  if ( !shortcuts [ contextName ] ) { // If contextName is not defined
40
- ev.emit ( '@shortcuts-error', `Context '${ contextName }' does not exist` )
41
+ ev.emit ( ERROR_EVENT_NAME, `Context '${ contextName }' does not exist` )
41
42
  return
42
43
  }
43
44
  if ( shortcuts[current] ) {
@@ -7,9 +7,10 @@ const
7
7
  API : { changeContext, getContext }
8
8
  } = dependencies;
9
9
  /**
10
- * Load a context with shortcuts object
11
- * @param {Object} shortcutsUpdate - Context description object: { contextName : { shortcut : callback[] }
12
- * @returns {void}
10
+ * @function load
11
+ * @description Load a context with shortcuts object
12
+ * @param {Object} shortcutsUpdate - Context description object: { contextName : { shortcut : callback[] } }
13
+ * @returns {void}
13
14
  */
14
15
  return function load ( shortcutsUpdate ) {
15
16
  const
@@ -2,7 +2,7 @@
2
2
 
3
3
  function unload ( dependencies, state ) {
4
4
  const
5
- { currentContext, shortcuts } = state
5
+ { currentContext, shortcuts, ERROR_EVENT_NAME } = state
6
6
  , { ev } = dependencies
7
7
  ;
8
8
  /**
@@ -14,11 +14,11 @@ const
14
14
  return function unload ( contextName ) {
15
15
  const current = currentContext.name;
16
16
  if ( current === contextName ) {
17
- ev.emit ( 'shortcuts-error', `Context '${ contextName }' can't be removed during is current active context. Change the context first` )
17
+ ev.emit ( ERROR_EVENT_NAME, `Context '${ contextName }' can't be removed during is current active context. Change the context first` )
18
18
  return
19
19
  }
20
20
  if ( !shortcuts [ contextName ] ) {
21
- ev.emit ( 'shortcuts-error', `Context '${ contextName }' does not exist` )
21
+ ev.emit ( ERROR_EVENT_NAME, `Context '${ contextName }' does not exist` )
22
22
  return
23
23
  }
24
24
  delete shortcuts [ contextName ]
@@ -38,7 +38,7 @@ function _listenDOM ( dependencies, state ) {
38
38
  ev.emit ( mouseEvent, data )
39
39
  // Reset:
40
40
  mouseTimer = null
41
- mouseIgnore = null
41
+ mouseIgnore = null // Timeout timer or null. Ignore mouse clicks until timer expires
42
42
  mouseTarget = null
43
43
  mouseDomEvent = null
44
44
  count = 0
@@ -9,6 +9,16 @@ import _registerShortcutEvents from "./_registerShortcutEvents"
9
9
 
10
10
 
11
11
 
12
+ /**
13
+ * @function pluginClick
14
+ * @description Plugin for mouse click shortcuts
15
+ * @param {Object} dependencies - Internal dependencies
16
+ * @param {Object} state - Library state
17
+ * @param {Object} [options={}] - Plugin options
18
+ * @param {number} [options.mouseWait=320] - Time to wait for click sequence in ms
19
+ * @param {string} [options.clickTarget='click'] - Data attribute name for click targets
20
+ * @returns {PluginAPI} Plugin API
21
+ */
12
22
  function pluginClick ( dependencies, state, options ) {
13
23
  let
14
24
  { currentContext, shortcuts } = state
@@ -8,20 +8,15 @@ import _defaults from './_defaults.js'
8
8
 
9
9
 
10
10
 
11
+ /**
12
+ * @function pluginForm
13
+ * @description Plugin for form element shortcuts
14
+ * @param {Object} dependencies - Internal dependencies
15
+ * @param {Object} state - Library state
16
+ * @param {Object} [options={}] - Plugin options
17
+ * @returns {PluginAPI} Plugin API
18
+ */
11
19
  function pluginForm ( dependencies, state, options ) {
12
- /**
13
- * 'form: watch' - A function. Should return a string. Define a selection that will be watched for changes. example: 'input, select.color, textarea, #name'
14
- * 'form: define' - A function that receives every watched element. Should return text value that represents the type of the
15
- * element according custom specification. Types could be specific for every single form.
16
- * 'form:action' - List of Callback objects.
17
- * Callback definition object:
18
- * {
19
- * fn: function to be called
20
- * type: fn should be executed on type of the element
21
- * timing: 'in' | 'out' | 'instant' - when to execute the function
22
- * wait: time in milliseconds to wait before executing the function again. Works only with mode 'instant'.
23
- * }
24
- */
25
20
 
26
21
  let
27
22
  { currentContext, shortcuts } = state
@@ -9,6 +9,16 @@ import _specialChars from './_specialChars.js'
9
9
 
10
10
 
11
11
 
12
+ /**
13
+ * @function pluginKey
14
+ * @description Plugin for keyboard shortcuts
15
+ * @param {Object} dependencies - Internal dependencies
16
+ * @param {Object} state - Library state
17
+ * @param {Object} [options={}] - Plugin options
18
+ * @param {number} [options.keyWait=480] - Time to wait for key sequence in ms
19
+ * @param {function} [options.streamKeys] - Function to stream key presses
20
+ * @returns {PluginAPI} Plugin API
21
+ */
12
22
  function pluginKey ( dependencies, state, options={} ) {
13
23
  let
14
24
  { currentContext, shortcuts, exposeShortcut } = state
@@ -37,7 +37,8 @@ const contextDefinition = {
37
37
  ' key : shift+a': [
38
38
  () => a = true,
39
39
  () => c = 'triggered'
40
- ]
40
+ ],
41
+ '@shortcuts-error' : ( m ) => c = m
41
42
  }
42
43
  , touch : {
43
44
  // Single click with left button
@@ -88,6 +89,64 @@ describe ( "Shortcuts", () => {
88
89
  }) // afterEach
89
90
 
90
91
 
92
+
93
+ it ( 'Load and unload context', async () => {
94
+ let ls = short.listContexts ()
95
+ expect ( ls ).to.includes ( 'general' )
96
+
97
+ short.changeContext ( 'general' )
98
+ // Can't not to unload current active context - sends an error on @shortcuts-error
99
+ // @shortcuts-error is a system error event used accross the library
100
+ short.unload ( 'general' )
101
+ expect ( c ).to.be.equal ( `Context 'general' can't be removed during is current active context. Change the context first` )
102
+
103
+ // Try to change context to non-existent one
104
+ // Message goes to @shortcuts-error
105
+ short.changeContext ( 'hhm' )
106
+ expect ( c ).to.be.equal ( `Context 'hhm' does not exist` )
107
+
108
+ // Change to something that exists
109
+ short.changeContext ( 'extra' )
110
+ // Free to unload 'general'
111
+ short.unload ( 'general' )
112
+ ls = short.listContexts ()
113
+ // Unload success
114
+ expect ( ls ).to.not.includes ( 'general' )
115
+ }) // it load and unload context
116
+
117
+
118
+
119
+ it ( 'List enabled plugins. Disable and enable plugins', async () => {
120
+ expect ( short.listPlugins () ).to.be.deep.equal ( [] )
121
+ // Enable list of plugins
122
+ let myPlugins = [ pluginKey, pluginClick ]
123
+ myPlugins.forEach ( plugin => short.enablePlugin ( plugin ) )
124
+ expect ( short.listPlugins () ).to.be.deep.equal ( [ 'key', 'click' ] )
125
+ // Method disablePlugin require plugin name (prefix)
126
+ short.disablePlugin ( 'click' )
127
+ expect ( short.listPlugins () ).to.be.deep.equal ( [ 'key' ] )
128
+ // Method enablePlugin require the plugin as a function
129
+ short.enablePlugin ( pluginClick )
130
+ expect ( short.listPlugins () ).to.be.deep.equal ( [ 'key', 'click' ] )
131
+ }) // it list enabled plugins
132
+
133
+
134
+
135
+ it ( 'Unload non existing context', () => {
136
+ let change = false;
137
+ let ls = short.listContexts ()
138
+ short.load ( {
139
+ local : {
140
+ 'click : leff-1' : () => console.log ( 'nothing' ),
141
+ '@shortcuts-error': () => change = true
142
+ }
143
+ })
144
+ short.changeContext ( 'local' )
145
+ short.unload ( 'unknown' )
146
+ expect ( change ).to.be.true
147
+ }) // it unload non existing context
148
+
149
+
91
150
 
92
151
  it ( 'Emit custom event', () => {
93
152
  // TODO: Check arguments for the custom event handlers
@@ -113,8 +172,8 @@ describe ( "Shortcuts", () => {
113
172
 
114
173
  let general = short.listShortcuts ('general');
115
174
  expect ( general ).to.be.an ( 'array' )
116
- expect ( general ).to.have.lengthOf ( 1 )
117
- expect ( general[0] ).to.be.equal ( 'KEY:A+SHIFT' )
175
+ expect ( general ).to.have.lengthOf ( 2 )
176
+ expect ( general ).to.include ( 'KEY:A+SHIFT' )
118
177
 
119
178
  let fail = short.listShortcuts ( 'somethingNotExisting' );
120
179
  expect ( fail ).to.be.null
@@ -127,8 +186,8 @@ describe ( "Shortcuts", () => {
127
186
  expect ( all[0] ).to.have.property ( 'context' )
128
187
  expect ( all[0] ).to.have.property ( 'shortcuts' )
129
188
  expect ( all[0].shortcuts ).to.be.an ( 'array' )
130
- expect ( all[0].shortcuts ).to.have.lengthOf ( 1 )
131
- expect ( all[0].shortcuts[0] ).to.be.equal ( 'KEY:A+SHIFT' )
189
+ expect ( all[0].shortcuts ).to.have.lengthOf ( 2 )
190
+ expect ( all[0].shortcuts ).to.include ( 'KEY:A+SHIFT' )
132
191
  expect ( all[0].context ).to.be.equal ( 'general' )
133
192
  }) // it list shortcuts
134
193