Skip to content
keybinds.ts 4.09 KiB
Newer Older
import EventEmitter from "eventemitter3";
import { EnforceObjectType } from "./utils";

interface IKeybind {
  key: KeyboardEvent["code"] | "LCLICK" | "RCLICK" | "MCLICK" | "LONG_PRESS";

  alt?: boolean;
  ctrl?: boolean;
  meta?: boolean;
  shift?: boolean;
}

interface EmittedKeybind {
  clientX: number;
  clientY: number;
}

export const enforceObjectType: EnforceObjectType<IKeybind[]> = (v) => v;
  PIXEL_WHOIS: [
    {
      key: "LCLICK",
      shift: true,
    },
    { key: "LONG_PRESS" },
  ],
  TEMPLATE_MOVE: [
    {
      key: "LCLICK",
      alt: true,
    },
  ],
Grant's avatar
Grant committed
  TOGGLE_TEMPLATE: [
    {
      key: "KeyT",
    },
  ],
Grant's avatar
Grant committed
  TOGGLE_BLANK: [
Grant's avatar
Grant committed
    {
Grant's avatar
Grant committed
      key: "KeyV", // legacy pxls keybind
    },
    {
      key: "KeyB",
Grant's avatar
Grant committed
    },
  ],
  TOGGLE_MOD_MENU: [
    {
      key: "KeyM",
    },
  ],
Grant's avatar
Grant committed
  DESELECT_COLOR: [
    {
      key: "Escape",
    },
  ],
Grant's avatar
Grant committed
  PICK_COLOR: [
    {
      key: "MCLICK",
    },
  ],
});

class KeybindManager_ extends EventEmitter<{
  [k in keyof typeof KEYBINDS]: (args: EmittedKeybind) => void;
}> {
  constructor() {
    super();
    // setup listeners

    document.addEventListener("keydown", this.handleKeydown, {
      passive: false,
    });
    document.addEventListener("keyup", this.handleKeyup);
Grant's avatar
Grant committed
    document.addEventListener("click", this.handleClick); // only gets triggered for left click
    document.addEventListener("auxclick", (e) => {
      if (e.button === 0) return; // left button still triggers this
      this.handleClick(e);
    });
  }

  destroy() {
    // remove listeners
    // this is global and doesn't depend on any elements, so this shouldn't need to be called
  }

  handleKeydown = (e: KeyboardEvent) => {
    const blacklistedElements = ["INPUT"];

    if (e.target instanceof HTMLElement) {
      if (blacklistedElements.indexOf(e.target.tagName) > -1) {
        return;
      }
    }

    if (e.key === "Alt") e.preventDefault();
    if (e.key === "Control") e.preventDefault();
    if (e.key === "Shift") e.preventDefault();
  };

  handleKeyup = (e: KeyboardEvent) => {
    // discard if in an input element

    const blacklistedElements = ["INPUT"];

    if (e.target instanceof HTMLElement) {
      if (blacklistedElements.indexOf(e.target.tagName) > -1) {
        return;
      }
    }

    let isHandled = this.handleInteraction(
      {
        key: e.code,
        alt: e.altKey,
        ctrl: e.ctrlKey,
        meta: e.metaKey,
        shift: e.shiftKey,
      },
      {
        clientX: -1,
        clientY: -1,
      }
    );

    if (isHandled) e.preventDefault();
  };

  handleClick = (e: MouseEvent) => {
    let button: "LCLICK" | "RCLICK" | "MCLICK" = ["LCLICK", "MCLICK", "RCLICK"][
      e.button
    ] as any;

    let isHandled = this.handleInteraction(
      {
        key: button,
        alt: e.altKey,
        ctrl: e.ctrlKey,
        meta: e.metaKey,
        shift: e.shiftKey,
      },
      {
        clientX: e.clientX,
        clientY: e.clientY,
      }
    );

    if (isHandled) e.preventDefault();
  };

  /**
   * Handle interaction
   * @param key
   * @returns if handled
   */
  handleInteraction(key: IKeybind, emit: EmittedKeybind): boolean {
    let isHandled = false;

    for (const [name_, keybinds] of Object.entries(KEYBINDS)) {
      const name: keyof typeof KEYBINDS = name_ as any;

      const valid = keybinds.find((kb) => {
        if (kb.key !== key.key) return false;
        if (typeof kb.alt !== "undefined" && kb.alt !== key.alt) return false;
        if (typeof kb.ctrl !== "undefined" && kb.ctrl !== key.ctrl)
          return false;
        if (typeof kb.meta !== "undefined" && kb.meta !== key.meta)
          return false;
        if (typeof kb.shift !== "undefined" && kb.shift !== key.shift)
          return false;

        return true;
      });

      if (!valid) continue;

      this.emit(name, emit);
      isHandled = true;
    }

    return isHandled;
  }

  getKeybind(key: keyof typeof KEYBINDS) {
    return KEYBINDS[key];
  }

  getKeybinds() {
    return { ...KEYBINDS };
  }
}

export const KeybindManager = new KeybindManager_();