keyboard-i18n
Version:
Internationalization and localization utils for keyboard shortcuts
254 lines (246 loc) • 7.26 kB
JavaScript
// src/helpers.ts
import {
identifyKeyboardLayout
} from "keyboard-layout-map";
function isKeyboardEventLike(event) {
return !!(event && typeof event === "object" && "key" in event && "code" in event && "shiftKey" in event && "altKey" in event && "ctrlKey" in event && "metaKey" in event && typeof event.key === "string" && typeof event.code === "string");
}
function layoutEquals(a, b) {
if (a === b) {
return true;
}
if (a && b && identifyKeyboardLayout(a) === identifyKeyboardLayout(b)) {
return true;
}
return false;
}
// src/localize.ts
import {
identifyKeyboardLayout as identifyKeyboardLayout2
} from "keyboard-layout-map";
// src/env.ts
import { getKeyboardLayout } from "keyboard-layout-map";
var isAppleOS = typeof window !== "undefined" && /Mac|iP(hone|[ao]d)/.test(window.navigator.platform);
var layout = null;
var updatedAt = 0;
function getLayout() {
void updateLayout();
return layout;
}
async function updateLayout() {
if (Date.now() - updatedAt < 3e3) {
return;
}
updatedAt = Date.now();
const newLayout = await getKeyboardLayout();
if (!layoutEquals(layout, newLayout)) {
layout = newLayout;
}
}
// src/parse.ts
function parseShortcut(shortcut, options) {
var _a;
const apple = (_a = options == null ? void 0 : options.isAppleOS) != null ? _a : isAppleOS;
let modifiers;
let target;
let alt = false;
let ctrl = false;
let shift = false;
let meta = false;
if (shortcut.endsWith("++")) {
modifiers = shortcut.slice(0, -2).split("+");
target = "+";
} else {
const parts = shortcut.split("+");
target = parts.pop();
modifiers = parts;
}
if (modifiers.includes("shift") || target.startsWith("Shift")) {
shift = true;
}
if (modifiers.includes("ctrl") || target.startsWith("Control")) {
ctrl = true;
}
if (modifiers.includes("alt") || target.startsWith("Alt")) {
alt = true;
}
if (modifiers.includes("meta") || target.startsWith("Meta")) {
meta = true;
}
if (modifiers.includes("mod")) {
if (apple) {
meta = true;
} else {
ctrl = true;
}
}
return { target, alt, ctrl, shift, meta };
}
// src/localize.ts
var defaultLocalizer = (shortcut, layout2) => {
var _a;
const layoutName = identifyKeyboardLayout2(layout2);
const target = (_a = localizeTarget(shortcut.target, layoutName)) != null ? _a : shortcut.target;
return { ...shortcut, target };
};
function localizeTarget(target, layout2) {
switch (target) {
case "BracketLeft":
switch (layout2) {
case "Spanish":
case "Italian":
case "German":
case "SwissGerman":
case "Finnish":
case "Portuguese":
case "Norwegian":
case "Danish":
return "Semicolon";
case "LatinAmerican":
return "Quote";
case "Dvorak":
return "[";
}
break;
case "BracketRight":
switch (layout2) {
case "Spanish":
case "Italian":
case "German":
case "SwissGerman":
case "Finnish":
case "Portuguese":
case "Norwegian":
case "Danish":
return "Quote";
case "LatinAmerican":
return "Backslash";
case "Dvorak":
return "]";
}
break;
case "Slash":
switch (layout2) {
case "French":
return "Period";
case "Portuguese":
case "German":
case "SwissGerman":
case "SwissFrench":
case "Italian":
case "LatinAmerican":
case "Spanish":
return "Minus";
case "Finnish":
case "Danish":
case "Norwegian":
case "Slovak":
return "BracketLeft";
case "Dvorak":
return "/";
}
break;
}
return null;
}
function wrapLocalizer(shortcut, options) {
const localizer = (options == null ? void 0 : options.localizer) || defaultLocalizer;
const parsed = parseShortcut(shortcut, {
isAppleOS: options == null ? void 0 : options.isAppleOS
});
getLayout();
let layout2 = null;
let localized = parsed;
return () => {
const currentLayout = (options == null ? void 0 : options.layout) || getLayout();
if (currentLayout !== layout2) {
layout2 = currentLayout;
localized = layout2 ? localizer(parsed, layout2) : parsed;
}
return localized;
};
}
// src/checker.ts
function createChecker(shortcut, options) {
const getLocalized = wrapLocalizer(shortcut, options);
return function checker(event) {
if (!isKeyboardEventLike(event)) {
return false;
}
const localized = getLocalized();
return eventMatches(event, localized);
};
}
function eventMatches(event, shortcut) {
if (event.isComposing) {
return false;
}
const key = event.key.toLowerCase();
const code = event.code.toLowerCase();
const target = shortcut.target.toLowerCase();
return (key === target || code === target) && event.altKey === shortcut.alt && event.ctrlKey === shortcut.ctrl && event.shiftKey === shortcut.shift && event.metaKey === shortcut.meta;
}
// src/formatter.ts
import { US as USKeyboardLayoutMap } from "keyboard-layout-map/layouts";
function createFormatter(shortcut, options) {
const getLocalized = wrapLocalizer(shortcut, options);
return function formatter() {
var _a, _b, _c;
const localized = getLocalized();
const layout2 = (_b = (_a = options == null ? void 0 : options.layout) != null ? _a : getLayout()) != null ? _b : USKeyboardLayoutMap;
const apple = (_c = options == null ? void 0 : options.isAppleOS) != null ? _c : isAppleOS;
return formatParsed(localized, layout2, apple);
};
}
function formatParsed(parsed, layout2, isAppleOS2) {
var _a;
const key = (_a = layout2.get(parsed.target)) != null ? _a : parsed.target;
return isAppleOS2 ? [
// Use the order of Control -> Option -> Shift -> Command
//
// Reference: https://support.apple.com/en-us/HT201236
parsed.ctrl ? "Control" : "",
parsed.alt ? "Option" : "",
parsed.shift ? "Shift" : "",
parsed.meta ? "Command" : "",
key
].filter(Boolean) : [
// Use the order of Win -> Ctrl -> Alt -> Shift
//
// Reference: https://support.microsoft.com/en-us/windows/keyboard-shortcuts-in-windows-dcc61a57-8ff0-cffe-9796-cb9706c75eec
parsed.meta ? "Win" : "",
parsed.ctrl ? "Ctrl" : "",
parsed.alt ? "Alt" : "",
parsed.shift ? "Shift" : "",
key
].filter(Boolean);
}
// src/handler.ts
import { getActiveElement, isHTMLElement } from "@zag-js/dom-query";
function createHandler(shortcut, callback, options) {
const shortcuts = Array.isArray(shortcut) ? shortcut : [shortcut];
const checkers = shortcuts.map(
(shortcut2) => createChecker(shortcut2, options)
);
return function handler(event) {
var _a;
for (const checker of checkers) {
if (checker(event)) {
if (event.key === "Dead") {
const target = event.target;
if (target && isHTMLElement(target)) {
(_a = getActiveElement(target)) == null ? void 0 : _a.blur();
}
}
callback(event);
return;
}
}
};
}
export {
createChecker,
createFormatter,
createHandler,
defaultLocalizer
};