您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Navigate (almost) any accessible website with a controller.
// ==UserScript== // @name Simple Gamepad Navigation // @namespace https://miniwangdali.github.io/simple-gamepad-navigation/ // @version 0.0.1 // @author [email protected] // @description Navigate (almost) any accessible website with a controller. // @license MIT // @icon https://raw.githubusercontent.com/miniwangdali/SimpleGamepadNavigation/refs/heads/main/packages/monkey-script/asset/icon.svg // @match *://*/* // @grant window.close // ==/UserScript== (function () { 'use strict'; var NavigationDirection; (function(NavigationDirection2) { NavigationDirection2["Up"] = "up"; NavigationDirection2["Down"] = "down"; NavigationDirection2["Left"] = "left"; NavigationDirection2["Right"] = "right"; })(NavigationDirection || (NavigationDirection = {})); var ScrollDirection; (function(ScrollDirection2) { ScrollDirection2["Vertical"] = "vertical"; ScrollDirection2["Horizontal"] = "horizontal"; })(ScrollDirection || (ScrollDirection = {})); const isSliderElement = (element) => { return !!element && element instanceof HTMLInputElement && (element.type === "range" || element.role === "slider"); }; const isTabListElement = (element) => { return element.role === "tablist"; }; const getTabListElementOfTarget = (target) => { let el = target; while (el) { if (el.previousSibling instanceof Element && isTabListElement(el.previousSibling)) { return el.previousSibling; } if (el.nextSibling instanceof Element && isTabListElement(el.nextSibling)) { return el.nextSibling; } if (isTabListElement(el)) { return el; } el = el.parentElement; } return el; }; const getTabItemsOfTabList = (tabList) => { return tabList.querySelectorAll('[role="tab"]'); }; function debounce(func, debounceMs, { signal, edges } = {}) { let pendingThis = void 0; let pendingArgs = null; const leading = edges != null && edges.includes("leading"); const trailing = edges == null || edges.includes("trailing"); const invoke = () => { if (pendingArgs !== null) { func.apply(pendingThis, pendingArgs); pendingThis = void 0; pendingArgs = null; } }; const onTimerEnd = () => { if (trailing) { invoke(); } cancel(); }; let timeoutId = null; const schedule = () => { if (timeoutId != null) { clearTimeout(timeoutId); } timeoutId = setTimeout(() => { timeoutId = null; onTimerEnd(); }, debounceMs); }; const cancelTimer = () => { if (timeoutId !== null) { clearTimeout(timeoutId); timeoutId = null; } }; const cancel = () => { cancelTimer(); pendingThis = void 0; pendingArgs = null; }; const flush = () => { invoke(); }; const debounced = function(...args) { if (signal?.aborted) { return; } pendingThis = this; pendingArgs = args; const isFirstCall = timeoutId == null; schedule(); if (leading && isFirstCall) { invoke(); } }; debounced.schedule = schedule; debounced.cancel = cancel; debounced.flush = flush; signal?.addEventListener("abort", cancel, { once: true }); return debounced; } function throttle(func, throttleMs, { signal, edges = ["leading", "trailing"] } = {}) { let pendingAt = null; const debounced = debounce(func, throttleMs, { signal, edges }); const throttled = function(...args) { if (pendingAt == null) { pendingAt = Date.now(); } else { if (Date.now() - pendingAt >= throttleMs) { pendingAt = Date.now(); debounced.cancel(); } } debounced(...args); }; throttled.cancel = debounced.cancel; throttled.flush = debounced.flush; return throttled; } const calculateDistanceOfTwoPoints = (pointA, pointB) => { const dx = pointB.x - pointA.x; const dy = pointB.y - pointA.y; return Math.sqrt(dx * dx + dy * dy); }; const DISTANCE_BUFFER = 2; const getDistanceOfTwoElements = (sourceElementRect, targetElementRect, side) => { switch (side) { case "top": { const sourceTopMiddlePoint = { x: (sourceElementRect.left + sourceElementRect.right) / 2, y: sourceElementRect.top }; const targetBottomMiddlePoint = { x: (targetElementRect.left + targetElementRect.right) / 2, y: targetElementRect.bottom }; let distance = calculateDistanceOfTwoPoints(sourceTopMiddlePoint, targetBottomMiddlePoint); if (sourceTopMiddlePoint.y < targetBottomMiddlePoint.y - DISTANCE_BUFFER) { distance = Infinity; } return distance; } case "bottom": { const sourceBottomMiddlePoint = { x: (sourceElementRect.left + sourceElementRect.right) / 2, y: sourceElementRect.bottom }; const targetTopMiddlePoint = { x: (targetElementRect.left + targetElementRect.right) / 2, y: targetElementRect.top }; let distance = calculateDistanceOfTwoPoints(sourceBottomMiddlePoint, targetTopMiddlePoint); if (sourceBottomMiddlePoint.y > targetTopMiddlePoint.y + DISTANCE_BUFFER) { distance = Infinity; } return distance; } case "left": { const sourceLeftMiddlePoint = { x: sourceElementRect.left, y: (sourceElementRect.top + sourceElementRect.bottom) / 2 }; const targetRightMiddlePoint = { x: targetElementRect.right, y: (targetElementRect.top + targetElementRect.bottom) / 2 }; let distance = calculateDistanceOfTwoPoints(sourceLeftMiddlePoint, targetRightMiddlePoint); if (sourceLeftMiddlePoint.x < targetRightMiddlePoint.x - DISTANCE_BUFFER) { distance = Infinity; } return distance; } case "right": { const sourceRightMiddlePoint = { x: sourceElementRect.right, y: (sourceElementRect.top + sourceElementRect.bottom) / 2 }; const targetLeftMiddlePoint = { x: targetElementRect.left, y: (targetElementRect.top + targetElementRect.bottom) / 2 }; let distance = calculateDistanceOfTwoPoints(sourceRightMiddlePoint, targetLeftMiddlePoint); if (sourceRightMiddlePoint.x > targetLeftMiddlePoint.x + DISTANCE_BUFFER) { distance = Infinity; } return distance; } } }; const isElementInRect = (elementRect, containerRect) => { return elementRect.top >= containerRect.top - DISTANCE_BUFFER && elementRect.bottom <= containerRect.bottom + DISTANCE_BUFFER && elementRect.left >= containerRect.left - DISTANCE_BUFFER && elementRect.right <= containerRect.right + DISTANCE_BUFFER; }; const isDialogElement = (element) => { return element instanceof HTMLDialogElement || element.role === "dialog" || element.getAttribute("aria-modal") === "true"; }; const getDialogElementOfTarget = (target) => { let el = target; while (el) { if (isDialogElement(el)) { return el; } el = el.parentElement; } return el; }; const INPUT_ROLES = ["textbox", "searchbox", "combobox", "slider", "spinbutton"]; const INTERACTABLE_ROLES = ["button", "link", "checkbox", "radio", "slider", "tab", ...INPUT_ROLES]; const isContentEditable = (element) => { return element.hasAttribute("contenteditable") && element.getAttribute("contenteditable") !== "false"; }; const hasInputRole = (element) => { return element.hasAttribute("role") && INPUT_ROLES.includes(element.getAttribute("role")); }; const hasInteractableRole = (element) => { return element.hasAttribute("role") && INTERACTABLE_ROLES.includes(element.getAttribute("role")); }; const isInputElement = (element) => { return element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || hasInputRole(element) || isContentEditable(element); }; const isVisibleElement = (element) => { if (!(element instanceof HTMLElement)) return false; const rect = element.getBoundingClientRect(); if (rect.width === 0 && rect.height === 0 && rect.x === 0 && rect.y === 0) return false; const style = getComputedStyle(element); return style.display !== "none" && style.visibility !== "hidden" && style.opacity !== "0"; }; const isInteractableElement = (element) => { return element instanceof HTMLButtonElement || element instanceof HTMLAnchorElement || isInputElement(element) || hasInteractableRole(element); }; const getInteractableElements = (restriction) => { const treeWalker = document.createTreeWalker(restriction?.restrictedRootElement || document.body, NodeFilter.SHOW_ELEMENT); const interactableElements = []; while (treeWalker.nextNode()) { const currentNode = treeWalker.currentNode; if ((currentNode instanceof Element || currentNode.nodeType === Node.ELEMENT_NODE) && isInteractableElement(currentNode)) { const interactableRect = currentNode.getBoundingClientRect(); const inRestrictedRect = !restriction?.restrictedRect || isElementInRect(interactableRect, restriction.restrictedRect); if (inRestrictedRect && isVisibleElement(currentNode)) { interactableElements.push(currentNode); } } } return interactableElements; }; const findNewInteractableElement = (currentElement, direction) => { const currentRect = currentElement.getBoundingClientRect(); const restrictedRootElement = getDialogElementOfTarget(currentElement); const allInteractableElements = getInteractableElements({ restrictedRootElement }); let candidate = void 0; let candidateDistance = Infinity; for (const element of allInteractableElements) { if (element === currentElement) continue; const elementRect = element.getBoundingClientRect(); let distance = 0; switch (direction) { case "up": distance = getDistanceOfTwoElements(currentRect, elementRect, "top"); break; case "down": distance = getDistanceOfTwoElements(currentRect, elementRect, "bottom"); break; case "left": distance = getDistanceOfTwoElements(currentRect, elementRect, "left"); break; case "right": distance = getDistanceOfTwoElements(currentRect, elementRect, "right"); break; } if (distance >= 0 && distance < candidateDistance) { candidate = element; candidateDistance = distance; } } return candidate; }; const getNearestScrollContainer = (element) => { let el = element; while (el) { if (el instanceof HTMLElement) { const style = getComputedStyle(el); const overflowY = style.overflowY; const isScrollableY = (overflowY === "auto" || overflowY === "scroll") && el.scrollHeight > el.clientHeight; const overflowX = style.overflowX; const isScrollableX = (overflowX === "auto" || overflowX === "scroll") && el.scrollWidth > el.clientWidth; if (isScrollableY || isScrollableX) { return el; } } el = el.parentElement; if (el === document.documentElement) { break; } } return el instanceof HTMLElement ? el : null; }; const canScroll = (scrollContainer, direction, speed) => { switch (direction) { case ScrollDirection.Vertical: { const reverse = getComputedStyle(scrollContainer).flexDirection === "column-reverse"; if (speed >= 0) { return reverse ? scrollContainer.scrollTop < 0 : scrollContainer.scrollTop + scrollContainer.clientHeight < scrollContainer.scrollHeight; } else if (speed < 0) { return reverse ? -scrollContainer.scrollTop + scrollContainer.clientHeight < scrollContainer.scrollHeight : scrollContainer.scrollTop > 0; } break; } case ScrollDirection.Horizontal: { const reverse = getComputedStyle(scrollContainer).flexDirection === "row-reverse"; if (speed >= 0) { return reverse ? scrollContainer.scrollLeft < 0 : scrollContainer.scrollLeft + scrollContainer.clientWidth < scrollContainer.scrollWidth; } else if (speed < 0) { return reverse ? -scrollContainer.scrollLeft + scrollContainer.clientWidth < scrollContainer.scrollWidth : scrollContainer.scrollLeft > 0; } break; } } return false; }; const isElementPositionedUnRelatedToScrollContainer = (element, scrollContainer) => { let el = element; let unrelatedPosition = false; const treeWalker = document.createTreeWalker(scrollContainer, NodeFilter.SHOW_ELEMENT); const elementsInScrollContainer = new Set(); while (treeWalker.nextNode()) { elementsInScrollContainer.add(treeWalker.currentNode); } while (el && elementsInScrollContainer.has(el)) { if (el === scrollContainer) { return false; } const position = getComputedStyle(el).position; if (position === "absolute" || position === "fixed") { unrelatedPosition = true; } el = el.offsetParent; } return unrelatedPosition; }; const THROTTLE_DELAY$1 = 250; const scroll = (direction, speed, originalPosition, immediate) => { if (document.activeElement) { const scrollContainer = getNearestScrollContainer(document.activeElement); if (scrollContainer) { originalPosition.x = scrollContainer.scrollLeft; originalPosition.y = scrollContainer.scrollTop; const style = getComputedStyle(scrollContainer); const overflowY = style.overflowY; const isScrollableY = (overflowY === "auto" || overflowY === "scroll" || scrollContainer === document.documentElement) && scrollContainer.scrollHeight > scrollContainer.clientHeight; const overflowX = style.overflowX; const isScrollableX = (overflowX === "auto" || overflowX === "scroll" || scrollContainer === document.documentElement) && scrollContainer.scrollWidth > scrollContainer.clientWidth; const scrollAmount = scrollContainer.clientHeight * speed; switch (direction) { case ScrollDirection.Vertical: if (isScrollableY) { scrollContainer.scrollBy({ top: scrollAmount, behavior: immediate ? "auto" : "smooth" }); } break; case ScrollDirection.Horizontal: if (isScrollableX) { scrollContainer.scrollBy({ left: scrollAmount, behavior: immediate ? "auto" : "smooth" }); } break; } return true; } } return false; }; const throttledScroll = throttle((direction, speed, state, immediate) => { state.result = scroll(direction, speed, state.originalPosition, immediate); }, THROTTLE_DELAY$1, { edges: ["leading"] }); const THROTTLE_DELAY = 250; const focusInteractableElement = (target) => { if (target instanceof HTMLElement) { document.activeElement.blur(); target.focus(); target.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "nearest" }); return true; } return false; }; const focusNextInteractableElement = (current, direction) => { let nextElement = findNewInteractableElement(current, direction); const scrollContainer = getNearestScrollContainer(current); const scrollDirection = direction === NavigationDirection.Up || direction === NavigationDirection.Down ? ScrollDirection.Vertical : ScrollDirection.Horizontal; const speed = direction === NavigationDirection.Up || direction === NavigationDirection.Left ? -0.5 : 0.5; while (true) { if (scrollContainer && nextElement && canScroll(scrollContainer, scrollDirection, speed) && isElementPositionedUnRelatedToScrollContainer(nextElement, scrollContainer)) { const originalPosition = { x: 0, y: 0 }; throttledScroll(scrollDirection, speed, { result: false, originalPosition }, true); const newNextElement = findNewInteractableElement(current, direction); if (newNextElement === nextElement) { scrollContainer.scrollTo(originalPosition.x, originalPosition.y); break; } nextElement = newNextElement; continue; } break; } if (nextElement instanceof HTMLElement) { return focusInteractableElement(nextElement); } return false; }; const navigate = (direction) => { const viewportRect = new DOMRect(0, 0, window.innerWidth, window.innerHeight); if (document.activeElement && document.activeElement !== document.body && isElementInRect(document.activeElement.getBoundingClientRect(), viewportRect)) { return focusNextInteractableElement(document.activeElement, direction); } else { const allInteractableElements = getInteractableElements({ restrictedRect: viewportRect }); let candidate; let candidateRect; switch (direction) { case NavigationDirection.Up: candidateRect = new DOMRect(Infinity, 0, 0, 0); break; case NavigationDirection.Right: case NavigationDirection.Down: candidateRect = new DOMRect(Infinity, Infinity, 0, 0); break; case NavigationDirection.Left: candidateRect = new DOMRect(0, Infinity, 0, 0); break; } for (const element of allInteractableElements) { const elementRect = element.getBoundingClientRect(); switch (direction) { case NavigationDirection.Up: if (elementRect.bottom > candidateRect.bottom || elementRect.bottom === candidateRect.bottom && elementRect.left < candidateRect.left) { candidate = element; candidateRect = elementRect; } break; case NavigationDirection.Right: if (elementRect.left < candidateRect.left || elementRect.left === candidateRect.left && elementRect.top < candidateRect.top) { candidate = element; candidateRect = elementRect; } break; case NavigationDirection.Down: if (elementRect.top < candidateRect.top || elementRect.top === candidateRect.top && elementRect.left < candidateRect.left) { candidate = element; candidateRect = elementRect; } break; case NavigationDirection.Left: if (elementRect.right > candidateRect.right || elementRect.right === candidateRect.right && elementRect.top < candidateRect.top) { candidate = element; candidateRect = elementRect; } break; } } if (candidate) { return focusInteractableElement(candidate); } } return false; }; const throttledNavigate = throttle((direction, state) => { state.result = navigate(direction); }, THROTTLE_DELAY, { edges: ["leading"] }); const LEFT_THUMBSTICK_DEFAULT_THRESHOLD = 0.6; const RIGHT_THUMBSTICK_DEFAULT_THRESHOLD = 0.4; var XboxButton; (function(XboxButton2) { XboxButton2[XboxButton2["A"] = 0] = "A"; XboxButton2[XboxButton2["B"] = 1] = "B"; XboxButton2[XboxButton2["X"] = 2] = "X"; XboxButton2[XboxButton2["Y"] = 3] = "Y"; XboxButton2[XboxButton2["LeftBumper"] = 4] = "LeftBumper"; XboxButton2[XboxButton2["RightBumper"] = 5] = "RightBumper"; XboxButton2[XboxButton2["LeftTrigger"] = 6] = "LeftTrigger"; XboxButton2[XboxButton2["RightTrigger"] = 7] = "RightTrigger"; XboxButton2[XboxButton2["View"] = 8] = "View"; XboxButton2[XboxButton2["Menu"] = 9] = "Menu"; XboxButton2[XboxButton2["LeftThumbStick"] = 10] = "LeftThumbStick"; XboxButton2[XboxButton2["RightThumbStick"] = 11] = "RightThumbStick"; XboxButton2[XboxButton2["DpadUp"] = 12] = "DpadUp"; XboxButton2[XboxButton2["DpadDown"] = 13] = "DpadDown"; XboxButton2[XboxButton2["DpadLeft"] = 14] = "DpadLeft"; XboxButton2[XboxButton2["DpadRight"] = 15] = "DpadRight"; XboxButton2[XboxButton2["Nexus"] = 16] = "Nexus"; })(XboxButton || (XboxButton = {})); const buttonKeyMap = { [XboxButton.A]: "Enter", [XboxButton.B]: "Escape", [XboxButton.LeftTrigger]: "Shift", [XboxButton.RightTrigger]: "Control", [XboxButton.DpadUp]: "ArrowUp", [XboxButton.DpadDown]: "ArrowDown", [XboxButton.DpadLeft]: "ArrowLeft", [XboxButton.DpadRight]: "ArrowRight" }; class XboxStandardController { constructor() { Object.defineProperty(this, "pressedKeys", { enumerable: true, configurable: true, writable: true, value: new Set() }); Object.defineProperty(this, "workingTabListElement", { enumerable: true, configurable: true, writable: true, value: null }); Object.defineProperty(this, "workingOnSlider", { enumerable: true, configurable: true, writable: true, value: false }); Object.defineProperty(this, "getValidGamepad", { enumerable: true, configurable: true, writable: true, value: (gamepadId) => { const gamepads = GamepadManager.getInstance().getGamepads(); if (!gamepads.has(gamepadId)) { return null; } return gamepads.get(gamepadId); } }); Object.defineProperty(this, "checkNavigation", { enumerable: true, configurable: true, writable: true, value: (gamepadId) => { const gamepad = this.getValidGamepad(gamepadId); if (!gamepad) { return false; } const x = gamepad.axes[0]; const y = gamepad.axes[1]; const resultState = { result: false }; if (x > LEFT_THUMBSTICK_DEFAULT_THRESHOLD) { throttledNavigate(NavigationDirection.Right, resultState); } else if (x < -LEFT_THUMBSTICK_DEFAULT_THRESHOLD) { throttledNavigate(NavigationDirection.Left, resultState); } if (y > RIGHT_THUMBSTICK_DEFAULT_THRESHOLD) { throttledNavigate(NavigationDirection.Down, resultState); } else if (y < -RIGHT_THUMBSTICK_DEFAULT_THRESHOLD) { throttledNavigate(NavigationDirection.Up, resultState); } const dpadUp = gamepad.buttons[XboxButton.DpadUp]; const dpadDown = gamepad.buttons[XboxButton.DpadDown]; const dpadLeft = gamepad.buttons[XboxButton.DpadLeft]; const dpadRight = gamepad.buttons[XboxButton.DpadRight]; if (!this.workingOnSlider) { if (dpadUp.pressed) { throttledNavigate(NavigationDirection.Up, resultState); } if (dpadDown.pressed) { throttledNavigate(NavigationDirection.Down, resultState); } if (dpadLeft.pressed) { throttledNavigate(NavigationDirection.Left, resultState); } if (dpadRight.pressed) { throttledNavigate(NavigationDirection.Right, resultState); } } return resultState.result; } }); Object.defineProperty(this, "checkScrolling", { enumerable: true, configurable: true, writable: true, value: (gamepadId) => { const gamepad = this.getValidGamepad(gamepadId); if (!gamepad) { return false; } const x = gamepad.axes[2]; const y = gamepad.axes[3]; const resultState = { result: false, originalPosition: { x: 0, y: 0 } }; if (x > RIGHT_THUMBSTICK_DEFAULT_THRESHOLD || x < -RIGHT_THUMBSTICK_DEFAULT_THRESHOLD) { throttledScroll(ScrollDirection.Horizontal, x, resultState); } if (y > RIGHT_THUMBSTICK_DEFAULT_THRESHOLD || y < -RIGHT_THUMBSTICK_DEFAULT_THRESHOLD) { throttledScroll(ScrollDirection.Vertical, y, resultState); } return resultState.result; } }); Object.defineProperty(this, "checkButtonKeyEvent", { enumerable: true, configurable: true, writable: true, value: (buttonIndex, gamepad) => { const button = gamepad.buttons[buttonIndex]; if (button.pressed) { const hasPressedKey = this.pressedKeys.has(buttonIndex); if (!hasPressedKey) { this.pressedKeys.add(buttonIndex); } const eventTarget = document.activeElement || document.body; if (!hasPressedKey) { if (eventTarget instanceof HTMLAnchorElement && buttonIndex === XboxButton.A) { eventTarget.click(); } } const enterKeyDownEvent = new KeyboardEvent("keydown", { key: buttonKeyMap[buttonIndex], bubbles: true }); eventTarget.dispatchEvent(enterKeyDownEvent); } else if (this.pressedKeys.has(buttonIndex)) { this.pressedKeys.delete(buttonIndex); if (buttonIndex === XboxButton.A && isSliderElement(document.activeElement)) { this.workingOnSlider = true; } if (this.workingOnSlider && buttonIndex === XboxButton.B) { this.workingOnSlider = false; } const eventTarget = document.activeElement || document.body; const enterKeyUpEvent = new KeyboardEvent("keyup", { key: buttonKeyMap[buttonIndex], bubbles: true }); eventTarget.dispatchEvent(enterKeyUpEvent); } } }); Object.defineProperty(this, "checkMenuButton", { enumerable: true, configurable: true, writable: true, value: (gamepadId) => { const gamepad = this.getValidGamepad(gamepadId); if (!gamepad) { return; } const menuButton = gamepad.buttons[XboxButton.Menu]; if (menuButton.pressed) { if (!this.pressedKeys.has(XboxButton.Menu)) { this.pressedKeys.add(XboxButton.Menu); const eventTarget = document.activeElement || document.body; const eventInit = { bubbles: true, cancelable: true, button: 2, buttons: 2, view: window }; eventTarget.dispatchEvent(new MouseEvent("mousedown", eventInit)); eventTarget.dispatchEvent(new MouseEvent("contextmenu", eventInit)); } } else if (this.pressedKeys.has(XboxButton.Menu)) { this.pressedKeys.delete(XboxButton.Menu); const eventTarget = document.activeElement || document.body; const eventInit = { bubbles: true, cancelable: true, button: 2, buttons: 0, view: window }; eventTarget.dispatchEvent(new MouseEvent("mouseup", eventInit)); } } }); Object.defineProperty(this, "checkViewButton", { enumerable: true, configurable: true, writable: true, value: (gamepadId) => { const gamepad = this.getValidGamepad(gamepadId); if (!gamepad) { return; } const viewButton = gamepad.buttons[XboxButton.View]; if (viewButton.pressed) { if (!this.pressedKeys.has(XboxButton.View)) { this.pressedKeys.add(XboxButton.View); } } else if (this.pressedKeys.has(XboxButton.View)) { this.pressedKeys.delete(XboxButton.View); if (this.pressedKeys.has(XboxButton.LeftTrigger)) { window.close(); } } } }); Object.defineProperty(this, "checkButton", { enumerable: true, configurable: true, writable: true, value: (gamepadId) => { const gamepad = this.getValidGamepad(gamepadId); if (!gamepad) { return; } this.checkButtonKeyEvent(XboxButton.A, gamepad); this.checkButtonKeyEvent(XboxButton.B, gamepad); this.checkButtonKeyEvent(XboxButton.LeftTrigger, gamepad); this.checkButtonKeyEvent(XboxButton.RightTrigger, gamepad); if (this.workingOnSlider) { this.checkButtonKeyEvent(XboxButton.DpadLeft, gamepad); this.checkButtonKeyEvent(XboxButton.DpadRight, gamepad); this.checkButtonKeyEvent(XboxButton.DpadUp, gamepad); this.checkButtonKeyEvent(XboxButton.DpadDown, gamepad); } this.checkMenuButton(gamepadId); this.checkViewButton(gamepadId); } }); Object.defineProperty(this, "checkBumper", { enumerable: true, configurable: true, writable: true, value: (bumper, gamepad) => { const button = gamepad.buttons[bumper]; if (button.pressed) { if (!this.pressedKeys.has(bumper)) { this.pressedKeys.add(bumper); } } else if (this.pressedKeys.has(bumper)) { this.pressedKeys.delete(bumper); if (this.pressedKeys.has(XboxButton.RightTrigger)) { if (bumper === XboxButton.LeftBumper) { history.back(); } else { history.forward(); } return true; } const tabList = this.workingTabListElement || (document.activeElement ? getTabListElementOfTarget(document.activeElement) : null); if (tabList) { this.workingTabListElement = tabList; const tabItems = getTabItemsOfTabList(tabList); const selectedTabIndex = Array.from(tabItems).findIndex((tab) => tab.getAttribute("aria-selected") === "true"); let nextTabItem = 0; if (bumper === XboxButton.LeftBumper) { nextTabItem = Math.max(0, selectedTabIndex - 1); } else { nextTabItem = Math.min(tabItems.length - 1, selectedTabIndex + 1); } tabItems.item(nextTabItem).click(); tabItems.item(nextTabItem).focus(); return nextTabItem !== selectedTabIndex; } } return false; } }); Object.defineProperty(this, "checkBumpers", { enumerable: true, configurable: true, writable: true, value: (gamepadId) => { const gamepad = this.getValidGamepad(gamepadId); if (!gamepad) { return; } let result = false; result || (result = this.checkBumper(XboxButton.LeftBumper, gamepad)); result || (result = this.checkBumper(XboxButton.RightBumper, gamepad)); return result; } }); } checkInput(gamepad) { const navigated = this.checkNavigation(gamepad.index); this.checkScrolling(gamepad.index); this.checkButton(gamepad.index); if (navigated) { this.workingTabListElement = null; this.workingOnSlider = false; } this.checkBumpers(gamepad.index); } } const defaultAdapter = new XboxStandardController(); class GamepadManager { static getInstance() { if (!GamepadManager.instance) { GamepadManager.instance = new GamepadManager(); } return GamepadManager.instance; } constructor() { Object.defineProperty(this, "gamepads", { enumerable: true, configurable: true, writable: true, value: new Map() }); Object.defineProperty(this, "pollingHandle", { enumerable: true, configurable: true, writable: true, value: null }); Object.defineProperty(this, "gamepadConnectedListener", { enumerable: true, configurable: true, writable: true, value: (e) => { const gamepad = navigator.getGamepads()[e.gamepad.index]; if (gamepad && (gamepad.mapping === "standard" || gamepad.mapping === "xr-standard") && !this.gamepads.has(gamepad.index)) { this.addGamepad(gamepad); } } }); Object.defineProperty(this, "gamepadDisconnectedListener", { enumerable: true, configurable: true, writable: true, value: (e) => { if (this.gamepads.has(e.gamepad.index)) { this.removeGamepad(e.gamepad); } } }); Object.defineProperty(this, "addGamepad", { enumerable: true, configurable: true, writable: true, value: (gamepad) => { this.gamepads.set(gamepad.index, gamepad); } }); Object.defineProperty(this, "removeGamepad", { enumerable: true, configurable: true, writable: true, value: (gamepad) => { this.gamepads.delete(gamepad.index); } }); Object.defineProperty(this, "updateGamepad", { enumerable: true, configurable: true, writable: true, value: (gamepad) => { this.gamepads.set(gamepad.index, gamepad); } }); Object.defineProperty(this, "startPolling", { enumerable: true, configurable: true, writable: true, value: () => { const gamepads = navigator.getGamepads().filter((gp) => this.gamepads.has(gp?.index ?? -1)); for (const gamepad of gamepads) { this.updateGamepad(gamepad); const adapter = defaultAdapter; adapter.checkInput(gamepad); } this.pollingHandle = requestAnimationFrame(this.startPolling); } }); Object.defineProperty(this, "stopPolling", { enumerable: true, configurable: true, writable: true, value: () => { if (this.pollingHandle !== null) { cancelAnimationFrame(this.pollingHandle); this.pollingHandle = null; } } }); this.initialize(); } initialize() { window.addEventListener("gamepadconnected", this.gamepadConnectedListener); window.addEventListener("gamepaddisconnected", this.gamepadDisconnectedListener); this.startPolling(); } getGamepads() { return this.gamepads; } cleanUp() { this.gamepads.clear(); this.stopPolling(); window.removeEventListener("gamepadconnected", this.gamepadConnectedListener); window.removeEventListener("gamepaddisconnected", this.gamepadDisconnectedListener); } } const tag = "[SimpleGamepadNavigation]"; let gamepadManager; const initializeGamepadNavigation = () => { const globalThis = window; if (globalThis.SimpleGamepadNavigation?.initialized) { console.info(`${tag} Already initialized.`); return; } window.addEventListener("load", () => { gamepadManager = GamepadManager.getInstance(); }); window.addEventListener("beforeunload", () => { gamepadManager.cleanUp(); }); if (!globalThis.SimpleGamepadNavigation) { globalThis.SimpleGamepadNavigation = { initialized: true }; } else { globalThis.SimpleGamepadNavigation.initialized = true; } console.info(`${tag} Initialized.`); }; initializeGamepadNavigation(); })();