дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 03:05:40 +02:00
Built-in Vim emulation on @replit/codemirror-vim (approved dependency), modelled on IdeaVim: `vim.toggle` (Tools › Vim, status widget, Settings › Editor › Vim), mode widget (NORMAL / INSERT / VISUAL / V-LINE / V-BLOCK / REPLACE), IdeaVim-style shortcut conflict handlers (Vim / IDE per key) through a generic `registerKeyClaim()` in the keymap dispatcher, Escape closing popups first, a safe subset of ~/.openideavimrc / ~/.ideavimrc (set, map variants, mapleader, :action / <Action>()), Ex commands :w :wa :q :qa :wq :x :action. Covers file editors, diff editable side and merge result via a new `embeddedEditorExtensions` extension point. Verified in the app on macOS: motions/operators, i/Esc/V mode widget, :w saves, Ctrl+R redoes in Vim without running Run, toggling off restores plain editing. Not verified on Windows/Linux. Fixes #27 https://claude.ai/code/session_01VUYsdXA2nkF5v6zsC93dpn
485 рядки
18 KiB
TypeScript
485 рядки
18 KiB
TypeScript
/**
|
|
* Keymaps and keyboard dispatch.
|
|
*
|
|
* Binding syntax: modifiers `Meta Ctrl Alt Shift` joined with `+` to a key, e.g.
|
|
* `Meta+Shift+O`, `Alt+F7`, `F8`. Space separates chord strokes
|
|
* (`Meta+K Meta+C`). A modifier pressed twice is `Shift Shift` (IDEA's double
|
|
* Shift, `Ctrl Ctrl`…); pressed twice and held, then a key, `Alt Alt+Up`.
|
|
* Keys are matched by physical position (`KeyboardEvent.code`), so ⌥-letter
|
|
* combos work on macOS where `event.key` would be a symbol.
|
|
*/
|
|
import { create } from "zustand";
|
|
import { getAction, isActionKeyEnabled, executeAction } from "./actions";
|
|
import { getContext } from "./context";
|
|
import { intellijMacos } from "./keymaps/intellij-macos";
|
|
import { intellijWindows } from "./keymaps/intellij-windows";
|
|
import { vscodeMacos } from "./keymaps/vscode-macos";
|
|
import { vscodeWindows } from "./keymaps/vscode-windows";
|
|
import { usePlatform } from "./platformInfo";
|
|
|
|
export type Keymap = Record<string, string[]>;
|
|
|
|
/** macOS spells the primary modifier ⌘ (`Meta`); Windows and Linux use Ctrl. */
|
|
export const isMacHost = () => usePlatform.getState().os === "macos";
|
|
|
|
/** The preset used when settings name none (or an unknown one): IDEA's keymap for this OS. */
|
|
export function defaultPreset(): string {
|
|
return isMacHost() ? "intellij-macos" : "intellij-windows";
|
|
}
|
|
|
|
export const PRESETS: Record<string, Keymap> = {
|
|
"intellij-macos": intellijMacos,
|
|
"intellij-windows": intellijWindows,
|
|
"vscode-macos": vscodeMacos,
|
|
"vscode-windows": vscodeWindows,
|
|
};
|
|
|
|
/** Display names of the presets, in menu order. */
|
|
export const PRESET_TITLES: Record<string, string> = {
|
|
"intellij-macos": "macOS",
|
|
"intellij-windows": "Windows",
|
|
"vscode-macos": "VS Code (macOS)",
|
|
"vscode-windows": "VS Code (Windows)",
|
|
};
|
|
|
|
/** ⌘ held on macOS, Ctrl elsewhere: the modifier of ⌘/Ctrl-click and copy/paste. */
|
|
export function hasPrimaryModifier(e: Pick<KeyboardEvent, "metaKey" | "ctrlKey">): boolean {
|
|
return isMacHost() ? e.metaKey : e.ctrlKey;
|
|
}
|
|
|
|
/** A binding written for macOS (`Meta+C`) as this OS spells it (`Ctrl+C` on Windows/Linux). */
|
|
export function hostBinding(binding: string): string {
|
|
return isMacHost() ? binding : binding.replace(/Meta\+/g, "Ctrl+");
|
|
}
|
|
|
|
/** `strokeOf` output in macOS spelling, so tables keyed `Meta+C` also match Ctrl+C on Windows/Linux. */
|
|
export function macStroke(stroke: string): string {
|
|
return isMacHost() ? stroke : normalizeBinding(stroke.replace(/Ctrl\+/g, "Meta+"));
|
|
}
|
|
|
|
interface KeymapState {
|
|
preset: string;
|
|
/** User overrides: action id → bindings (empty array removes defaults). */
|
|
overrides: Keymap;
|
|
}
|
|
|
|
export const useKeymap = create<KeymapState>(() => ({ preset: defaultPreset(), overrides: {} }));
|
|
|
|
const STROKE_RE = /^(?:(?:Ctrl|Alt|Shift|Meta)\+)*[^\s+]+$|^(?:(?:Ctrl|Alt|Shift|Meta)\+)*\+$/i;
|
|
|
|
const MODIFIERS = ["Ctrl", "Alt", "Shift", "Meta"] as const;
|
|
export type Modifier = (typeof MODIFIERS)[number];
|
|
const DOUBLE_RE = /^(Ctrl|Alt|Shift|Meta) (Ctrl|Alt|Shift|Meta)(?:\+([^\s+]+|\+))?$/i;
|
|
const modifierNamed = (name: string) => MODIFIERS.find((m) => m.toLowerCase() === name.toLowerCase())!;
|
|
|
|
/** A double-modifier gesture: `Shift Shift` (key null) or `Alt Alt+Up`. */
|
|
export function parseDoubleModifier(binding: string): { modifier: Modifier; key: string | null } | null {
|
|
const m = DOUBLE_RE.exec(binding.trim());
|
|
if (!m || m[1]!.toLowerCase() !== m[2]!.toLowerCase()) return null;
|
|
const key = m[3] ?? null;
|
|
return { modifier: modifierNamed(m[1]!), key: key && key.length === 1 ? key.toUpperCase() : key };
|
|
}
|
|
|
|
/** Syntactic check: one or two strokes of `Mod+…+Key`, or a double-modifier gesture. */
|
|
export function isValidBinding(binding: unknown): binding is string {
|
|
if (typeof binding !== "string") return false;
|
|
const b = binding.trim();
|
|
if (parseDoubleModifier(b)) return true;
|
|
const strokes = b.split(/\s+/);
|
|
return strokes.length >= 1 && strokes.length <= 2 && strokes.every((s) => STROKE_RE.test(s));
|
|
}
|
|
|
|
/**
|
|
* Applies keymap settings (preset id + per-action overrides) to `useKeymap`.
|
|
* Input comes from settings files, so it is validated: an unknown preset falls
|
|
* back to the default, malformed bindings are dropped.
|
|
*/
|
|
export function applyKeymapSettings(preset: unknown, overrides: unknown, customKeymaps?: unknown): void {
|
|
// A user keymap (`customKeymaps[id]`) is its parent preset plus its own overrides.
|
|
const custom =
|
|
typeof preset === "string" && customKeymaps && typeof customKeymaps === "object"
|
|
? ((customKeymaps as Record<string, unknown>)[preset] as { parent?: unknown; overrides?: unknown } | undefined)
|
|
: undefined;
|
|
if (custom && typeof custom === "object" && !PRESETS[preset as string]) {
|
|
preset = custom.parent;
|
|
overrides = custom.overrides;
|
|
}
|
|
const id = typeof preset === "string" && PRESETS[preset] ? preset : defaultPreset();
|
|
const clean: Keymap = {};
|
|
if (overrides && typeof overrides === "object" && !Array.isArray(overrides)) {
|
|
for (const [action, keys] of Object.entries(overrides as Record<string, unknown>)) {
|
|
if (!Array.isArray(keys)) continue;
|
|
clean[action] = [...new Set(keys.filter(isValidBinding).map(normalizeBinding))];
|
|
}
|
|
}
|
|
const prev = useKeymap.getState();
|
|
if (prev.preset === id && JSON.stringify(prev.overrides) === JSON.stringify(clean)) return;
|
|
useKeymap.setState({ preset: id, overrides: clean });
|
|
}
|
|
|
|
let suspended = 0;
|
|
|
|
/**
|
|
* Decides whether a keystroke belongs to the focused component rather than to
|
|
* the IDE keymap (e.g. Vim mode's shortcut conflicts set to "Vim"). `stroke`
|
|
* is the canonical first stroke (`Ctrl+R`).
|
|
*/
|
|
export type KeyClaim = (e: KeyboardEvent, stroke: string) => boolean;
|
|
|
|
const keyClaims = new Set<KeyClaim>();
|
|
|
|
/** Registers a key claim; the dispatcher leaves claimed keystrokes to their target. */
|
|
export function registerKeyClaim(claim: KeyClaim): () => void {
|
|
keyClaims.add(claim);
|
|
return () => void keyClaims.delete(claim);
|
|
}
|
|
|
|
function isClaimed(e: KeyboardEvent, stroke: string): boolean {
|
|
for (const claim of keyClaims)
|
|
try {
|
|
if (claim(e, stroke)) return true;
|
|
} catch (err) {
|
|
console.error("key claim failed", err);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Pauses the global keyboard dispatcher (e.g. while recording a shortcut).
|
|
* Returns a function that resumes it; calls nest.
|
|
*/
|
|
export function suspendKeyboardDispatch(): () => void {
|
|
suspended++;
|
|
let done = false;
|
|
return () => {
|
|
if (done) return;
|
|
done = true;
|
|
suspended--;
|
|
};
|
|
}
|
|
|
|
export function bindingsFor(actionId: string): string[] {
|
|
const { preset, overrides } = useKeymap.getState();
|
|
return overrides[actionId] ?? PRESETS[preset]?.[actionId] ?? [];
|
|
}
|
|
|
|
/** Every (binding, actionId) pair of the effective keymap. */
|
|
export function effectiveBindings(): Array<[string, string]> {
|
|
const { preset, overrides } = useKeymap.getState();
|
|
const merged: Keymap = { ...PRESETS[preset], ...overrides };
|
|
return Object.entries(merged).flatMap(([id, keys]) => keys.map((k) => [normalizeBinding(k), id] as [string, string]));
|
|
}
|
|
|
|
const MOD_ORDER = MODIFIERS;
|
|
|
|
/** Canonical form: modifiers in fixed order, so `Shift+Meta+o` == `Meta+Shift+O`. */
|
|
export function normalizeBinding(binding: string): string {
|
|
const double = parseDoubleModifier(binding);
|
|
if (double) return `${double.modifier} ${double.modifier}${double.key ? `+${double.key}` : ""}`;
|
|
return binding
|
|
.trim()
|
|
.split(/\s+/)
|
|
.map((stroke) => {
|
|
const parts = stroke.split("+");
|
|
const key = parts.pop()!;
|
|
const mods = MOD_ORDER.filter((m) => parts.some((p) => p.toLowerCase() === m.toLowerCase()));
|
|
return [...mods, key.length === 1 ? key.toUpperCase() : key].join("+");
|
|
})
|
|
.join(" ");
|
|
}
|
|
|
|
const CODE_NAMES: Record<string, string> = {
|
|
Space: "Space",
|
|
Enter: "Enter",
|
|
NumpadEnter: "Enter",
|
|
Escape: "Escape",
|
|
Tab: "Tab",
|
|
Backspace: "Backspace",
|
|
Delete: "Delete",
|
|
Insert: "Insert",
|
|
Home: "Home",
|
|
End: "End",
|
|
PageUp: "PageUp",
|
|
PageDown: "PageDown",
|
|
ArrowUp: "Up",
|
|
ArrowDown: "Down",
|
|
ArrowLeft: "Left",
|
|
ArrowRight: "Right",
|
|
BracketLeft: "[",
|
|
BracketRight: "]",
|
|
Comma: ",",
|
|
Period: ".",
|
|
Slash: "/",
|
|
Backslash: "\\",
|
|
Semicolon: ";",
|
|
Quote: "'",
|
|
Backquote: "`",
|
|
Minus: "-",
|
|
Equal: "=",
|
|
NumpadAdd: "+",
|
|
NumpadSubtract: "-",
|
|
NumpadMultiply: "*",
|
|
NumpadDivide: "/",
|
|
};
|
|
|
|
/** Converts a keydown event into a canonical stroke, or null for bare modifiers. */
|
|
export function strokeOf(
|
|
e: Pick<KeyboardEvent, "code" | "key" | "metaKey" | "ctrlKey" | "altKey" | "shiftKey">,
|
|
): string | null {
|
|
let key: string | undefined;
|
|
if (/^Key[A-Z]$/.test(e.code)) key = e.code.slice(3);
|
|
else if (/^Digit\d$/.test(e.code)) key = e.code.slice(5);
|
|
else if (/^Numpad\d$/.test(e.code)) key = e.code.slice(6);
|
|
else if (/^F\d{1,2}$/.test(e.code)) key = e.code;
|
|
else key = CODE_NAMES[e.code];
|
|
if (!key) {
|
|
if (["Shift", "Meta", "Control", "Alt"].includes(e.key)) return null;
|
|
key = e.key.length === 1 ? e.key.toUpperCase() : e.key;
|
|
}
|
|
const mods = [e.ctrlKey && "Ctrl", e.altKey && "Alt", e.shiftKey && "Shift", e.metaKey && "Meta"].filter(Boolean);
|
|
return [...mods, key].join("+");
|
|
}
|
|
|
|
const SYMBOLS: Record<string, string> = {
|
|
Meta: "⌘",
|
|
Ctrl: "⌃",
|
|
Alt: "⌥",
|
|
Shift: "⇧",
|
|
Enter: "↵",
|
|
Escape: "⎋",
|
|
Tab: "⇥",
|
|
Backspace: "⌫",
|
|
Delete: "⌦",
|
|
Up: "↑",
|
|
Down: "↓",
|
|
Left: "←",
|
|
Right: "→",
|
|
Space: "Space",
|
|
PageUp: "⇞",
|
|
PageDown: "⇟",
|
|
};
|
|
|
|
/**
|
|
* Human display. macOS: `Meta+Shift+O` → `⇧⌘O`, `Shift Shift` → `⇧⇧`, `Alt Alt+Up` → `⌥⌥↑`.
|
|
* Windows/Linux: `Ctrl+Shift+N`, chords as `Ctrl+K, Ctrl+I`, `Shift, Shift`, `Ctrl, Ctrl+Up`.
|
|
*/
|
|
export function formatBinding(binding: string, mac = isMacHost()): string {
|
|
if (!mac) return normalizeBinding(binding).split(" ").join(", ");
|
|
const double = parseDoubleModifier(binding);
|
|
if (double) return SYMBOLS[double.modifier]!.repeat(2) + (double.key ? (SYMBOLS[double.key] ?? double.key) : "");
|
|
return normalizeBinding(binding)
|
|
.split(" ")
|
|
.map((stroke) => {
|
|
const parts = stroke.split("+");
|
|
const key = parts.pop()!;
|
|
// macOS menu order: ⌃⌥⇧⌘
|
|
return parts.map((p) => SYMBOLS[p] ?? p).join("") + (SYMBOLS[key] ?? key);
|
|
})
|
|
.join(" ");
|
|
}
|
|
|
|
export function shortcutLabel(actionId: string): string | undefined {
|
|
const b = bindingsFor(actionId)[0];
|
|
return b ? formatBinding(b) : undefined;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Dispatcher
|
|
|
|
/** How long IDEA waits for the second stroke of a chord. */
|
|
const CHORD_TIMEOUT = 2000;
|
|
/** IDEA's double-modifier timing: each press/release step. */
|
|
const DOUBLE_TAP_STEP_MS = 300;
|
|
/** A modifier tap this soon after typing another key is part of typing, not a gesture. */
|
|
const DOUBLE_TAP_TYPING_GUARD_MS = 100;
|
|
|
|
const KEY_MODIFIER: Record<string, Modifier> = { Shift: "Shift", Control: "Ctrl", Alt: "Alt", Meta: "Meta" };
|
|
|
|
function modifierHeld(e: Pick<KeyboardEvent, "ctrlKey" | "altKey" | "shiftKey" | "metaKey">, m: Modifier): boolean {
|
|
return m === "Ctrl" ? e.ctrlKey : m === "Alt" ? e.altKey : m === "Shift" ? e.shiftKey : e.metaKey;
|
|
}
|
|
|
|
const otherModifiers = (e: Pick<KeyboardEvent, "ctrlKey" | "altKey" | "shiftKey" | "metaKey">, m: Modifier) =>
|
|
MODIFIERS.some((x) => x !== m && modifierHeld(e, x));
|
|
|
|
function isPlainTextField(target: EventTarget | null): boolean {
|
|
const el = target as HTMLElement | null;
|
|
if (!el || !el.tagName) return false;
|
|
if (el.closest?.(".cm-editor")) return false;
|
|
return el.tagName === "INPUT" || el.tagName === "TEXTAREA" || el.isContentEditable;
|
|
}
|
|
|
|
function isTextTarget(target: EventTarget | null): boolean {
|
|
const el = target as HTMLElement | null;
|
|
if (!el || !el.tagName) return false;
|
|
return !!el.closest?.(".cm-editor") || el.tagName === "INPUT" || el.tagName === "TEXTAREA" || el.isContentEditable;
|
|
}
|
|
|
|
/**
|
|
* AltGr (reported as Ctrl+Alt on Windows) typing a character into a text field
|
|
* or editor, e.g. `}` on a German or `ł` on a Polish layout: the character wins
|
|
* over a Ctrl+Alt shortcut on the same key, as in IDEA.
|
|
*/
|
|
export function isAltGrTyping(
|
|
e: Pick<KeyboardEvent, "key" | "code" | "target" | "metaKey" | "ctrlKey" | "altKey" | "shiftKey"> & {
|
|
getModifierState?: (key: string) => boolean;
|
|
},
|
|
): boolean {
|
|
if (isMacHost() || !e.getModifierState?.("AltGraph")) return false;
|
|
if (e.key !== "Dead" && e.key.length !== 1) return false;
|
|
if (!isTextTarget(e.target)) return false;
|
|
// Ctrl+Alt+L on a layout with no AltGr character there still reports `l`: that is a shortcut.
|
|
const key = strokeOf(e)?.split("+").pop();
|
|
return e.key === "Dead" || e.key.toUpperCase() !== key;
|
|
}
|
|
|
|
function runIfEnabled(id: string): boolean {
|
|
const action = getAction(id);
|
|
if (!action || !isActionKeyEnabled(action)) return false;
|
|
void executeAction(id);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Installs the global keydown dispatcher (capture phase, so IDE shortcuts win
|
|
* over CodeMirror defaults). Returns an uninstall function. `now` is the clock
|
|
* for the double-modifier gestures.
|
|
*/
|
|
export function installKeyboardDispatcher(target: Window = window, now: () => number = () => performance.now()): () => void {
|
|
let pending: string[] = [];
|
|
let pendingTimer: ReturnType<typeof setTimeout> | undefined;
|
|
/**
|
|
* Double modifier: step 1 pressed, 2 released, 3 pressed again. A bare
|
|
* gesture (`Shift Shift`) fires on the second release; while the second
|
|
* press is held, keys fire their `Alt Alt+Up` gestures.
|
|
*/
|
|
let tap: { modifier: Modifier; step: 1 | 2 | 3; at: number; typed: boolean; keyUsed: boolean } | null = null;
|
|
let otherKeyAt = -Infinity;
|
|
|
|
const reset = () => {
|
|
pending = [];
|
|
clearTimeout(pendingTimer);
|
|
};
|
|
const resetTap = () => {
|
|
tap = null;
|
|
};
|
|
|
|
const onModifierDown = (e: KeyboardEvent, modifier: Modifier) => {
|
|
const t = now();
|
|
if (e.repeat && tap?.step === 3 && tap.modifier === modifier) return;
|
|
if (e.repeat || otherModifiers(e, modifier)) return resetTap();
|
|
const typed = t - otherKeyAt < DOUBLE_TAP_TYPING_GUARD_MS;
|
|
if (tap && (tap.modifier !== modifier || t - tap.at > DOUBLE_TAP_STEP_MS)) resetTap();
|
|
if (!tap) tap = { modifier, step: 1, at: t, typed, keyUsed: false };
|
|
else if (tap.step === 2) tap = { ...tap, step: 3, at: t, typed: tap.typed || typed };
|
|
else resetTap();
|
|
};
|
|
|
|
/** A key pressed while the second press of a modifier is held: its `Alt Alt+Up` gesture. */
|
|
const runHeldGesture = (e: KeyboardEvent): boolean => {
|
|
if (tap?.step !== 3 || !modifierHeld(e, tap.modifier) || otherModifiers(e, tap.modifier)) return false;
|
|
const key = strokeOf(e)?.split("+").pop();
|
|
if (!key) return false;
|
|
const binding = normalizeBinding(`${tap.modifier} ${tap.modifier}+${key}`);
|
|
const ids = effectiveBindings().filter(([b]) => b === binding).map(([, id]) => id);
|
|
for (const id of ids.reverse()) {
|
|
const action = getAction(id);
|
|
if (!action || !isActionKeyEnabled(action)) continue;
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
tap.keyUsed = true;
|
|
void executeAction(id);
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
const onKeyDown = (e: KeyboardEvent) => {
|
|
if (suspended > 0) {
|
|
reset();
|
|
resetTap();
|
|
return;
|
|
}
|
|
const modifier = KEY_MODIFIER[e.key];
|
|
if (modifier) return onModifierDown(e, modifier);
|
|
if (runHeldGesture(e)) return reset();
|
|
resetTap();
|
|
otherKeyAt = e.key === "Escape" || e.key === "Tab" ? -Infinity : now();
|
|
if (e.isComposing) return;
|
|
const stroke = strokeOf(e);
|
|
if (!stroke) return;
|
|
const chord = pending.length > 0;
|
|
if (!chord) {
|
|
if (isAltGrTyping(e)) return;
|
|
const hasCommandMod = e.metaKey || e.ctrlKey || e.altKey;
|
|
if (!hasCommandMod && isPlainTextField(e.target) && !/^(Shift\+)?(F\d{1,2}|Escape)$/.test(stroke)) return;
|
|
if (isClaimed(e, stroke)) return;
|
|
}
|
|
|
|
const seq = [...pending, stroke].join(" ");
|
|
const bindings = effectiveBindings();
|
|
const ctx = getContext();
|
|
const exact = bindings.filter(([b]) => b === seq).map(([, id]) => id);
|
|
// Later presets/overrides win; the first enabled match runs.
|
|
for (const id of exact.reverse()) {
|
|
const action = getAction(id);
|
|
if (action && isActionKeyEnabled(action, ctx)) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
reset();
|
|
void executeAction(id);
|
|
return;
|
|
}
|
|
}
|
|
if (bindings.some(([b]) => b.startsWith(seq + " "))) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
pending.push(stroke);
|
|
clearTimeout(pendingTimer);
|
|
pendingTimer = setTimeout(reset, CHORD_TIMEOUT);
|
|
return;
|
|
}
|
|
// A second stroke that completes no chord is swallowed, as in IDEA, not typed.
|
|
if (chord) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
}
|
|
reset();
|
|
};
|
|
|
|
const onKeyUp = (e: KeyboardEvent) => {
|
|
const modifier = KEY_MODIFIER[e.key];
|
|
if (suspended > 0 || !modifier || !tap || tap.modifier !== modifier) return;
|
|
const t = now();
|
|
if (otherModifiers(e, modifier) || t - tap.at > DOUBLE_TAP_STEP_MS) return resetTap();
|
|
if (tap.step === 1) {
|
|
tap = { ...tap, step: 2, at: t };
|
|
return;
|
|
}
|
|
const fire = tap.step === 3 && !tap.keyUsed && !tap.typed;
|
|
resetTap();
|
|
if (!fire) return;
|
|
const ids = effectiveBindings().filter(([b]) => b === `${modifier} ${modifier}`).map(([, id]) => id);
|
|
for (const id of ids.reverse()) if (runIfEnabled(id)) return;
|
|
};
|
|
|
|
// Shift+click (extend a selection) is not half of a double Shift.
|
|
const onMouseDown = () => resetTap();
|
|
|
|
const onBlur = () => {
|
|
reset();
|
|
resetTap();
|
|
};
|
|
|
|
target.addEventListener("keydown", onKeyDown, true);
|
|
target.addEventListener("keyup", onKeyUp, true);
|
|
target.addEventListener("mousedown", onMouseDown, true);
|
|
target.addEventListener("blur", onBlur);
|
|
return () => {
|
|
target.removeEventListener("keydown", onKeyDown, true);
|
|
target.removeEventListener("keyup", onKeyUp, true);
|
|
target.removeEventListener("mousedown", onMouseDown, true);
|
|
target.removeEventListener("blur", onBlur);
|
|
};
|
|
}
|