@tanstack/hotkeys
Version:
Type-safe, framework-agnostic keyboard hotkey management for the browser
84 lines (83 loc) • 3.36 kB
JavaScript
//#region src/_registration.ts
/** The scope of hotkeys that do not name one. It is always active unless an exclusive scope is active. */
const GLOBAL_SCOPE = "global";
/**
* Default options for hotkey/sequence registration.
* Omitted: platform, target (resolved at registration), requireReset (HotkeyManager only).
*/
const defaultHotkeyOptions = {
capture: false,
preventDefault: true,
stopPropagation: true,
eventType: "keydown",
enabled: true,
ignoreInputs: "auto",
conflictBehavior: "warn",
scope: GLOBAL_SCOPE
};
/**
* Merges option layers left to right. A key set to `undefined` keeps the
* earlier layer's value, so an unset option keeps its default. Getters are
* read once.
*/
function definedOptions(...layers) {
const result = {};
for (const layer of layers) {
if (!layer) continue;
for (const key of Object.keys(layer)) {
const value = layer[key];
if (value !== void 0) result[key] = value;
}
}
return result;
}
/**
* Merges options over defaults for an API whose `setOptions` treats
* `undefined` as "clear". An unset option takes its default, or stays
* `undefined` to clear an earlier value when it has no default.
*/
function withDefaults(defaults, options) {
return {
...options,
...definedOptions(defaults, options)
};
}
/** Whether a parsed chord has a key. An empty one would match key-less events. */
function chordHasKey(chord) {
return chord.code !== void 0 || !!chord.key;
}
/**
* Resolves `ignoreInputs: 'auto'` from the hotkey (the first step of a sequence).
* Ctrl/Meta shortcuts and Escape fire in inputs; single keys and Shift/Alt combos are ignored.
*/
function getDefaultIgnoreInputs(parsedChord) {
if (parsedChord.ctrl || parsedChord.meta) return false;
if (parsedChord.key === "Escape" || parsedChord.code === "Escape") return false;
return true;
}
/**
* Handles conflicts between registrations based on conflict behavior.
*
* @param conflictingId - The ID of the conflicting registration
* @param keyDisplay - Display string for the conflicting key/sequence (for error messages)
* @param conflictBehavior - How to handle the conflict
* @param unregister - Function to unregister by ID
*/
function handleConflict(conflictingId, keyDisplay, conflictBehavior, unregister) {
if (conflictBehavior === "allow") return;
if (conflictBehavior === "warn") {
if (process.env.NODE_ENV !== "production") console.warn(`'${keyDisplay}' is already registered. Multiple handlers may be triggered. Use conflictBehavior: 'replace' to replace the existing handler, or conflictBehavior: 'allow' to suppress this warning.`);
return;
}
if (conflictBehavior === "error") throw new Error(`'${keyDisplay}' is already registered. Use conflictBehavior: 'replace' to replace the existing handler, or conflictBehavior: 'allow' to allow multiple registrations.`);
unregister(conflictingId);
}
/** Compare updates without publishing unchanged options, including fresh metadata objects. */
function optionsEqual(left, right) {
const a = left;
const b = right;
const keys = Object.keys(a);
return keys.length === Object.keys(b).length && keys.every((key) => key === "meta" && a[key] && b[key] ? optionsEqual(a[key], b[key]) : Object.is(a[key], b[key]));
}
//#endregion
export { GLOBAL_SCOPE, chordHasKey, defaultHotkeyOptions, definedOptions, getDefaultIgnoreInputs, handleConflict, optionsEqual, withDefaults };