Equicord/src/equicordplugins/commandPalette/ui/keyboard.ts
Jake 4134b9f4f8
CommandPalette: Rewrite with modular API and Raycast-style UI (#1165)
* CommandPalette: Rewrite with modular API and Raycast-style UI

Replace the monolithic palette with a command/page registry, capture-phase keyboard routing, fuzzy search with pins and aliases, form pages including Send DM with markdown preview, and a top-anchored shell layout.

* CommandPalette: Fix stylelint errors in palette CSS

Reorder selectors for no-descending-specificity, replace deprecated word-break, and use overflow shorthand on picker list.

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-06-10 14:39:22 -04:00

140 lines
4.2 KiB
TypeScript

/*
* Vencord, a Discord client mod
* Copyright (c) 2026 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { IS_MAC } from "@utils/constants";
const MODIFIERS = ["meta", "ctrl", "shift", "alt"] as const;
export function comboFromEvent(e: KeyboardEvent): string[] | null {
const key = e.key.toLowerCase();
if (key === "meta" || key === "control" || key === "shift" || key === "alt") return null;
const combo: string[] = [];
if (e.metaKey) combo.push("meta");
if (e.ctrlKey) combo.push("ctrl");
if (e.shiftKey) combo.push("shift");
if (e.altKey) combo.push("alt");
combo.push(key === " " ? "space" : key);
return combo;
}
export function comboEquals(a: string[], b: string[]): boolean {
if (a.length !== b.length) return false;
const normalize = (combo: string[]) => [...combo].map(k => k.toLowerCase()).sort().join("+");
return normalize(a) === normalize(b);
}
const MAC_SYMBOLS: Record<string, string> = {
meta: "\u2318",
ctrl: "\u2303",
shift: "\u21E7",
alt: "\u2325"
};
const KEY_SYMBOLS: Record<string, string> = {
enter: "\u21B5",
arrowup: "\u2191",
arrowdown: "\u2193",
arrowleft: "\u2190",
arrowright: "\u2192",
backspace: "\u232B",
escape: "esc"
};
export function formatComboKeys(combo: string[]): string[] {
return combo.map(key => {
const lower = key.toLowerCase();
if (IS_MAC && MAC_SYMBOLS[lower]) return MAC_SYMBOLS[lower];
if (!IS_MAC && (MODIFIERS as readonly string[]).includes(lower)) {
return lower === "meta" ? "Win" : lower[0].toUpperCase() + lower.slice(1);
}
if (KEY_SYMBOLS[lower]) return KEY_SYMBOLS[lower];
return lower.length === 1 ? lower.toUpperCase() : lower[0].toUpperCase() + lower.slice(1);
});
}
export function formatCombo(combo: string[]): string {
return formatComboKeys(combo).join(IS_MAC ? "" : "+");
}
const EDITING_KEYS = new Set(["a", "c", "v", "x", "z", "y", "arrowleft", "arrowright", "arrowup", "arrowdown", "backspace", "delete", "home", "end"]);
function isEditingCombo(e: KeyboardEvent): boolean {
return EDITING_KEYS.has(e.key.toLowerCase());
}
export function isEditableTarget(target: EventTarget | null): boolean {
if (!(target instanceof HTMLElement)) return false;
return Boolean(target.closest("input, textarea, select, [contenteditable=true], [role=textbox]"));
}
type PaletteKeyHandler = (e: KeyboardEvent) => boolean;
type GlobalKeyHandler = (e: KeyboardEvent) => boolean;
let paletteHandler: PaletteKeyHandler | null = null;
let globalHandler: GlobalKeyHandler | null = null;
let hotkeysSuspended = false;
export function setHotkeysSuspended(suspended: boolean) {
hotkeysSuspended = suspended;
}
export function setPaletteKeyHandler(handler: PaletteKeyHandler | null) {
paletteHandler = handler;
}
export function setGlobalKeyHandler(handler: GlobalKeyHandler | null) {
globalHandler = handler;
}
export function isPaletteCapturing() {
return paletteHandler !== null;
}
function handleKeyDown(e: KeyboardEvent) {
if (paletteHandler) {
if (paletteHandler(e)) {
e.preventDefault();
e.stopImmediatePropagation();
return;
}
if (isEditableTarget(e.target) || isEditableTarget(document.activeElement)) return;
if ((e.metaKey || e.ctrlKey) && !isEditingCombo(e)) {
e.preventDefault();
e.stopImmediatePropagation();
}
return;
}
if (hotkeysSuspended) return;
if (globalHandler?.(e)) {
e.preventDefault();
e.stopPropagation();
}
}
function handleKeyUp(e: KeyboardEvent) {
if (!paletteHandler) return;
if (isEditableTarget(e.target) || isEditableTarget(document.activeElement)) return;
if ((e.metaKey || e.ctrlKey) && !isEditingCombo(e)) {
e.stopImmediatePropagation();
}
}
export function installKeyboardListeners() {
window.addEventListener("keydown", handleKeyDown, true);
window.addEventListener("keyup", handleKeyUp, true);
}
export function removeKeyboardListeners() {
window.removeEventListener("keydown", handleKeyDown, true);
window.removeEventListener("keyup", handleKeyUp, true);
paletteHandler = null;
globalHandler = null;
}