Simple Gamepad Navigation

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();

})();
长期地址
遇到问题?请前往 GitHub 提 Issues。