coherent-gameface-interaction-manager
Version:
Library for the most common UI interactions
3 lines (2 loc) • 17.3 kB
JavaScript
var spatialNavigation=(()=>{var f=(c,e)=>()=>(c&&(e=c(c=0)),e);var H=(c,e)=>()=>(e||c((e={exports:{}}).exports,e),e.exports);function F(c){return c*180/Math.PI}var S=f(()=>{});var E,o,m=f(()=>{E=class c{constructor(){this.actions=[],this.keyboardFunctions=[],this.gamepadFunctions=[]}init(){window._IM||(window._IM=new c)}getKeys(e){return _IM.keyboardFunctions.filter(t=>t.keys.every(i=>e.includes(i)))}getKeysIndex(e){return _IM.keyboardFunctions.findIndex(t=>t.keys.every(i=>e.includes(i)))}getGamepadAction({actions:e,type:t}){return _IM.gamepadFunctions.find(i=>i.actions.every(s=>e.includes(s))&&i.type===t&&i.actions.length===e.length)}getGamepadActions(e){return _IM.gamepadFunctions.filter(t=>t.actions.every(i=>e.includes(i))&&t.actions.length===e.length)}getGamepadActionIndex(e){return _IM.gamepadFunctions.findIndex(t=>t.actions.every(i=>e.includes(i)))}getAction(e){return _IM.actions.find(t=>t.name===e)}getActionIndex(e){return _IM.actions.findIndex(t=>t.name===e)}},o=new E});var _,y,A=f(()=>{m();_=class{register(e,t){if(o.getAction(e))return console.error(`The following action "${e}" is already registered!`);_IM.actions.push({name:e,callback:t})}remove(e){let t=o.getActionIndex(e);if(t===-1)return console.error(`${e} is not a registered action!`);_IM.actions.splice(t,1)}execute(e,t){let i=o.getAction(e);if(!i)return console.error(`${e} is not a registered action!`);i.callback(t)}},y=new _});var O,I=f(()=>{O={ALT:18,ARROW_DOWN:40,ARROW_LEFT:37,ARROW_RIGHT:39,ARROW_UP:38,BACKSPACE:8,CAPS_LOCK:20,CTRL:17,DELETE:46,END:35,ENTER:13,ESC:27,F1:112,F2:113,F3:114,F4:115,F5:116,F6:117,F7:118,F8:119,F9:120,F10:121,F11:122,F12:123,HOME:36,INSERT:45,NUM_LOCK:144,NUMPAD_ENTER:13,NUMPAD_DASH:109,NUMPAD_STAR:106,NUMPAD_DOT:110,NUMPAD_FORWARD_SLASH:111,NUMPAD_PLUS:107,NUMPAD_0:96,NUMPAD_1:97,NUMPAD_2:98,NUMPAD_3:99,NUMPAD_4:100,NUMPAD_5:101,NUMPAD_6:102,NUMPAD_7:103,NUMPAD_8:104,NUMPAD_9:105,PAGE_DOWN:34,PAGE_UP:33,PAUSE:19,PRINT_SCRN:44,SCROLL_LOCK:145,SHIFT:16,SPACEBAR:32,TAB:9,A:65,B:66,C:67,D:68,E:69,F:70,G:71,H:72,I:73,J:74,K:75,L:76,M:77,N:78,O:79,P:80,Q:81,R:82,S:83,T:84,U:85,V:86,W:87,X:88,Y:89,Z:90,1:49,2:50,3:51,4:52,5:53,6:54,7:55,8:56,9:57,0:48,QUOTE:222,DASH:189,COMMA:188,DOT:190,FORWARD_SLASH:191,SEMI_COLON:186,SQUARE_BRACKET_LEFT:219,SQUARE_BRACKET_RIGHT:221,BACKWARD_SLASH:220,BACKTICK:192,EQUAL:187,SYSTEM:91}});var x,v,U=f(()=>{m();I();A();x=class{constructor(){this.mappings=O,this.eventListenerAttached=!1,this.keysPressed=new Set,this.onKeyDown=this.onKeyDown.bind(this),this.onKeyUp=this.onKeyUp.bind(this),window.KEYS||(window.KEYS=O)}on(e){e.keys=[...new Set(e.keys.map(i=>(i=typeof i=="number"?this.keyCodeToString(i):i,i.toUpperCase())))];let t=e.keys.filter(i=>!this.mappings[i]);if(t.length>0)return console.error(`The following keys [${t.join(", ")}] you have entered are incorrect! `);this.eventListenerAttached||(document.addEventListener("keydown",this.onKeyDown),document.addEventListener("keyup",this.onKeyUp),this.eventListenerAttached=!0),Array.isArray(e.type)||(e.type=[e.type]),e.type.forEach(i=>{let s=o.getKeys(e.keys);if(s.length>0&&s.some(n=>n.type===i))return console.error("You are trying to overwrite an existing key combination! To do that, first remove it with .off([keys]) then add it again");if(i==="lift"&&e.keys.length>1)return console.error("You can only have a single key trigger an action on lift");_IM.keyboardFunctions.push({...e,type:i})})}off(e){e=[...new Set(e.map(i=>(i=typeof i=="number"?this.keyCodeToString(i):i,i.toUpperCase())))];let t=o.getKeys(e).length;if(t===0)return console.error("You are trying to remove a non-existent key combination!");for(;t>0;){let i=o.getKeysIndex(e);_IM.keyboardFunctions.splice(i,1),t--}_IM.keyboardFunctions.length===0&&(document.removeEventListener("keydown",this.onKeyDown),document.removeEventListener("keyup",this.onKeyUp),this.eventListenerAttached=!1)}onKeyDown(e){let t=this.keyCodeToString(e.keyCode);this.keysPressed.add(t);let i=o.getKeys([...this.keysPressed]);i.length!==0&&i.forEach(s=>{s.type==="press"&&e.repeat||s.type!=="press"&&s.type!=="hold"||s.type==="hold"&&!e.repeat||this.executeCallback(e,s)})}onKeyUp(e){let t=this.keyCodeToString(e.keyCode);this.keysPressed.delete(t);let i=o.getKeys(t);i.length!==0&&i.forEach(s=>{s.type==="lift"&&s.keys.indexOf(t)!==-1&&this.executeCallback(e,s)})}keyCodeToString(e){return Object.keys(this.mappings).find(t=>this.mappings[t]===e)}executeCallback(e,t){if(typeof t.callback=="string")return y.execute(t.callback,e);t.callback(e)}},v=new x});var M,L=f(()=>{M={FACE_BUTTON_DOWN:0,FACE_BUTTON_RIGHT:1,FACE_BUTTON_LEFT:2,FACE_BUTTON_TOP:3,LEFT_SHOULDER:4,RIGHT_SHOULDER:5,LEFT_SHOULDER_BOTTOM:6,RIGHT_SHOULDER_BOTTOM:7,SELECT:8,START:9,LEFT_ANALOGUE_STICK:10,RIGHT_ANALOGUE_STICK:11,PAD_UP:12,PAD_DOWN:13,PAD_LEFT:14,PAD_RIGHT:15,CENTER_BUTTON:16,aliases:{"face-button-down":"FACE_BUTTON_DOWN","face-button-left":"FACE_BUTTON_LEFT","face-button-right":"FACE_BUTTON_RIGHT","face-button-top":"FACE_BUTTON_TOP","left-sholder":"LEFT_SHOULDER","right-sholder":"RIGHT_SHOULDER","left-sholder-bottom":"LEFT_SHOULDER_BOTTOM","right-sholder-bottom":"RIGHT_SHOULDER_BOTTOM",select:"SELECT",start:"START","left-analogue-stick":"LEFT_ANALOGUE_STICK","right-analogue-stick":"RIGHT_ANALOGUE_STICK","pad-up":"PAD_UP","pad-down":"PAD_DOWN","pad-left":"PAD_LEFT","pad-right":"PAD_RIGHT","center-button":"CENTER_BUTTON","playstation.x":"FACE_BUTTON_DOWN","playstation.square":"FACE_BUTTON_LEFT","playstation.circle":"FACE_BUTTON_RIGHT","playstation.triangle":"FACE_BUTTON_TOP","playstation.l1":"LEFT_SHOULDER","playstation.r1":"RIGHT_SHOULDER","playstation.l2":"LEFT_SHOULDER_BOTTOM","playstation.r2":"RIGHT_SHOULDER_BOTTOM","playstation.share":"SELECT","playstation.options":"START","playstation.l3":"LEFT_ANALOGUE_STICK","playstation.r3":"RIGHT_ANALOGUE_STICK","playstation.d-pad-up":"PAD_UP","playstation.d-pad-down":"PAD_DOWN","playstation.d-pad-left":"PAD_LEFT","playstation.d-pad-right":"PAD_RIGHT","playstation.center":"CENTER_BUTTON","xbox.a":"FACE_BUTTON_DOWN","xbox.x":"FACE_BUTTON_LEFT","xbox.b":"FACE_BUTTON_RIGHT","xbox.y":"FACE_BUTTON_TOP","xbox.lb":"LEFT_SHOULDER","xbox.rb":"RIGHT_SHOULDER","xbox.lt":"LEFT_SHOULDER_BOTTOM","xbox.rt":"RIGHT_SHOULDER_BOTTOM","xbox.view":"SELECT","xbox.menu":"START","xbox.left-thumbstick":"LEFT_ANALOGUE_STICK","xbox.right-thumbstick":"RIGHT_ANALOGUE_STICK","xbox.d-pad-up":"PAD_UP","xbox.d-pad-down":"PAD_DOWN","xbox.d-pad-left":"PAD_LEFT","xbox.d-pad-right":"PAD_RIGHT","xbox.center":"CENTER_BUTTON"},axisAliases:["right.joystick","left.joystick","left.joystick.down","left.joystick.up","left.joystick.left","left.joystick.right","right.joystick.down","right.joystick.up","right.joystick.left","right.joystick.right"]}});var p,B,k,C,N=f(()=>{L();m();A();p=.9,B=["press","hold"],k=class{constructor(){this.mappings=M,this.pollingStarted=!1,this.gamepadEnabled=!1,this.onGamepadConnected=this.onGamepadConnected.bind(this),this.sanitizeAction=this.sanitizeAction.bind(this),this.pollingInterval=200,this._pressedAction=null}set enabled(e){this.gamepadEnabled=e,this.gamepadEnabled?this.init():this.deinit()}init(){window.addEventListener("gamepadconnected",this.onGamepadConnected)}deinit(){window.removeEventListener("gamepadconnected",this.onGamepadConnected)}onGamepadConnected(){this.pollingStarted||(this.pollingStarted=!0,this.startPolling())}on(e){e.actions=e.actions.map(this.sanitizeAction);let t=this.mappings.axisAliases.some(i=>e.actions.includes(i));if((!e.type||!B.includes(e.type))&&(e.type="hold"),e.type==="press"&&t)return console.error("You can't use an axis action with a 'press' type!");if(e.actions.length>1&&t)return console.error("You can't use an axis action in a combination with a button action");if(o.getGamepadAction(e))return console.error("You have already registered a callback for this action. If you want to overwrite it, remove it first with .off([actions])");_IM.gamepadFunctions.push(e)}off(e){let t=o.getGamepadActionIndex(e.map(this.sanitizeAction));if(t===-1)return console.error("You are trying to remove a non-existent action!");_IM.gamepadFunctions.splice(t,1)}startPolling(){let e=navigator.getGamepads();if(e.length===0){this.pollingStarted=!1;return}e.forEach((t,i)=>{t&&(this.handleButtons(t.buttons,i),this.handleJoysticks(t.axes))}),setTimeout(this.startPolling.bind(this),this.pollingInterval)}handleButtons(e){let t=e.reduce((s,n,r)=>(n.pressed&&(s.buttonIndexes.push(r),s.buttons.push(n)),s),{buttonIndexes:[],buttons:[]}),i=o.getGamepadActions(t.buttonIndexes);!i.length!==0&&(this._pressedAction&&(i.includes(this._pressedAction)||this.executeCallback(this._pressedAction,this._pressedAction.actions),this._pressedAction=null),i.forEach(s=>{if(s.type==="press"){this._pressedAction=s;return}this.executeCallback(s,t.buttons)}))}handleJoysticks(e){this.getJoystickActions().forEach(i=>{switch(i.actions[0]){case"left.joystick":return this.executeCallback(i,[e[0],e[1]]);case"right.joystick":return this.executeCallback(i,[e[2],e[3]]);case"left.joystick.down":if(e[1]>p)return this.executeCallback(i,[e[0],e[1]]);break;case"left.joystick.up":if(e[1]<-p)return this.executeCallback(i,[e[0],e[1]]);break;case"left.joystick.left":if(e[0]<-p)return this.executeCallback(i,[e[0],e[1]]);break;case"left.joystick.right":if(e[0]>p)return this.executeCallback(i,[e[0],e[1]]);break;case"right.joystick.down":if(e[3]>p)return this.executeCallback(i,[e[2],e[3]]);break;case"right.joystick.up":if(e[3]<-p)return this.executeCallback(i,[e[2],e[3]]);break;case"right.joystick.left":if(e[2]<-p)return this.executeCallback(i,[e[2],e[3]]);break;case"right.joystick.right":if(e[2]>p)return this.executeCallback(i,[e[2],e[3]]);break}})}sanitizeAction(e){if(typeof e=="number")return e;if(this.mappings.axisAliases.includes(e.toLowerCase()))return e.toLowerCase();if(typeof e=="string"){let t=this.mappings.aliases[e.toLowerCase()];return t?this.mappings[t]:console.error(`You have entered a non-supported button alias ${e}`)}return e}getJoystickActions(){return _IM.gamepadFunctions.filter(e=>this.mappings.axisAliases.includes(e.actions[0]))}executeCallback(e,t){if(typeof e.callback=="string")return y.execute(e.callback,t);e.callback(t)}},C=new k});var w,R,D,P,K=f(()=>{S();A();U();N();w=["down","up","left","right"],R={up:["arrow_up"],down:["arrow_down"],right:["arrow_right"],left:["arrow_left"]},D=class{constructor(){this.enabled=!1,this.navigatableElements={default:{elements:[],distance:0,overflow:{x:0,y:0}}},this.registeredKeys=new Set,this.clearCurrentActiveKeys=!1,this.overlapPercentage=.5,this.lastFocusedElement=null}init(e=[],t){this.enabled||(this.enabled=!0,this.add(e),this.activeKeys=JSON.parse(JSON.stringify(R)),this.registerKeyActions(),t&&0<=t&&t<=1&&(this.overlapPercentage=t))}deinit(){this.enabled&&(this.enabled=!1,this.navigatableElements={default:{elements:[],distance:0,overflow:{x:0,y:0}}},this.removeKeyActions(),this.overlapPercentage=.5,this.lastFocusedElement=null)}add(e){this.enabled&&e.forEach(t=>{typeof t=="string"?this.handleString(t):this.handleObject(t)})}remove(e="default"){if(this.enabled){if(!this.navigatableElements[e])return console.error(`The area '${e}' you are trying to remove doesn't exist`);this.navigatableElements[e].elements.forEach(t=>t.removeAttribute("tabindex")),this.navigatableElements[e]={}}}handleString(e){let t=document.querySelectorAll(e);if(t.length===0)return console.error(`${e} is either not a correct selector or the element is not present in the DOM.`);t.forEach(this.makeFocusable),this.setNavigationAreaProperties("default",t)}handleObject(e){let t=e.elements.reduce((i,s)=>{let n=document.querySelectorAll(s);return n.forEach(this.makeFocusable),i.push(...n),i},[]);if(t.length===0)return console.error(`${e.elements.join(", ")} are either not a correct selectors or the elements are not present in the DOM.`);this.navigatableElements[e.area]||(this.navigatableElements[e.area]={elements:[],distance:0}),this.setNavigationAreaProperties(e.area,t)}setNavigationAreaProperties(e,t){this.navigatableElements[e].elements.push(...t),this.navigatableElements[e].distance=this.getElementsDistance(this.navigatableElements[e].elements),this.navigatableElements[e].overflow=this.setOverflowValues(t[0].parentElement)}getElementsDistance(e){return e.reduce((t,i)=>{let{x:s,y:n}=i.getBoundingClientRect(),r=Math.hypot(s,n);return t<r?r:t},0)}setOverflowValues(e){if(!e)return{x:0,y:0};let{scrollWidth:t,scrollHeight:i}=e,s=Math.max(0,t-window.innerWidth),n=Math.max(0,i-window.innerHeight);return s>0||n>0?{x:s,y:n}:this.setOverflowValues(e.parentElement)}makeFocusable(e){e.setAttribute("tabindex",1)}getFocusableGroup(e,t,i){return t.reduce((s,n)=>{if(n!==e&&!n.hasAttribute("disabled")){let{x:r,y:a,height:u,width:d}=n.getBoundingClientRect();s.push({element:n,x:r+i,y:a+i,height:u,width:d})}return s},[])}getCurrentArea(e){return Object.values(this.navigatableElements).find(t=>{if(t.elements.includes(e))return!0})}getClosestToEdge(e,t,i,s,n){let r,a,u=window.innerHeight+s+n.y,d=window.innerWidth+s+n.x;return t.reduce((l,h)=>{switch(e){case"down":r=Math.hypot(h.x-i.x,h.y),a=Math.hypot(l.x-i.x,l.y);break;case"up":r=Math.hypot(h.x-i.x,u-h.y),a=Math.hypot(l.x-i.x,u-l.y);break;case"right":r=Math.hypot(h.x,h.y-i.y),a=Math.hypot(l.x,l.y-i.y);break;case"left":r=Math.hypot(d-h.x,h.y-i.y),a=Math.hypot(d-l.x,l.y-i.y);break}return l=r<a?h:l,l})}moveFocus(e){if(!this.enabled)return;let t=this.checkActiveElementInGroup(),i=this.getCurrentArea(t);if(!i)return console.error("The active element is not in a focusable area!");let{elements:s,distance:n,overflow:r}=i,a=this.getFocusableGroup(t,s,n),{x:u,y:d,width:l,height:h}=t.getBoundingClientRect(),T={x:u+n,y:d+n,width:l,height:h};if(a.length===0)return;let b=this.filterGroupByCurrentAxis(e,a,T);if(!b.length)return;let g=this.findNextElement(e,b,T.x,T.y);g||(g=this.getClosestToEdge(e,b,T,n,r)),g&&(g.element.focus(),this.lastFocusedElement=g.element)}filterGroupByCurrentAxis(e,t,i){return t.filter(s=>e==="left"||e==="right"?this.isOverlappingX(i,s):this.isOverlappingY(i,s))}isOverlappingX(e,t){let i=Math.min(e.y+e.height,t.y+t.height),s=Math.max(e.y,t.y),n=Math.max(0,i-s),r=Math.min(e.height,t.height);return n/r>=this.overlapPercentage}isOverlappingY(e,t){let i=Math.min(e.x+e.width,t.x+t.width),s=Math.max(e.x,t.x),n=Math.max(0,i-s),r=Math.min(e.width,t.width);return n/r>=this.overlapPercentage}findNextElement(e,t,i,s){return t.reduce((n,r)=>{let a=r.x-i,u=r.y-s,d=F(Math.atan2(u,a));if(this.getDirectionAngle(e,d)){n||(n=r);let l=Math.hypot(a,u),h=Math.hypot(n.x-i,n.y-s);n=l<h?r:n}return n},null)}getDirectionAngle(e,t){switch(e){case"down":return t>0&&t<180;case"up":return t>-180&&t<0;case"left":return t<-90||t>90;case"right":return t>-90&&t<90}}registerKeyActions(){w.forEach(e=>{let t=()=>{this.moveFocus(e)};y.register(`move-focus-${e}`,t);let i=this.activeKeys[e];for(let s of i)v.on({keys:[s],callback:`move-focus-${e}`,type:["press","hold"]}),this.registeredKeys.add(s);C.on({actions:[`playstation.d-pad-${e}`],callback:`move-focus-${e}`})})}resetKeys(){this.removeKeyActions(),this.activeKeys=JSON.parse(JSON.stringify(R)),this.registerKeyActions()}changeKeys(e,t={clearCurrentActiveKeys:!1}){let i=Object.keys(e);if(i.length===0)return;let s=i.filter(n=>!w.includes(n));if(s.length>0)return console.error(`The following directions: [${s.join(", ")}] you have entered are incorrect! `);this.clearCurrentActiveKeys=t.clearCurrentActiveKeys,this.removeKeyActions();for(let n in this.activeKeys){let r=e[n];typeof r=="string"&&!this.activeKeys[n].includes(r)&&this.activeKeys[n].push(r.toUpperCase())}this.registerKeyActions()}removeKeyActions(){this.registeredKeys.size!==0&&(this.registeredKeys.forEach(e=>v.off([e])),this.registeredKeys.clear(),w.forEach(e=>{y.remove(`move-focus-${e}`),C.off([`playstation.d-pad-${e}`]),this.clearCurrentActiveKeys&&(this.activeKeys[e]=[])}))}focusFirst(e="default"){let t=this.navigatableElements[e].elements;if(!t||t.length===0)return console.error(`The area '${e}' you are trying to focus doesn't exist or the spatial navigation hasn't been initialized`);if(this.lastFocusedElement=t.find(i=>!i.hasAttribute("disabled")),!this.lastFocusedElement)return console.error(`The area '${e}' you are trying to focus doesn't have any focusable elements`);this.lastFocusedElement.focus()}focusLast(e="default"){if(!this.enabled)return;let t=this.navigatableElements[e].elements;if(!t||t.length===0)return console.error(`The area '${e}' you are trying to focus doesn't exist or the spatial navigation hasn't been initialized`);let i;for(let s=t.length-1;s>=0;s--)if(!t[s].hasAttribute("disabled")){i=t[s];break}if(this.lastFocusedElement=i,!this.lastFocusedElement)return console.error(`The area '${e}' you are trying to focus doesn't have any focusable elements`);this.lastFocusedElement.focus()}switchArea(e){this.focusFirst(e)}isActiveElementInGroup(){return Object.values(this.navigatableElements).some(e=>e.elements.includes(document.activeElement))}checkActiveElementInGroup(){return this.isActiveElementInGroup(document.activeElement)?document.activeElement:this.lastFocusedElement}clearFocus(){this.isActiveElementInGroup()&&document.activeElement.blur()}},P=new D});var j=H((de,G)=>{K();m();o.init();G.exports=P});return j();})();
//# sourceMappingURL=spatial-navigation.min.js.map