@nuxt/ui
Version:
A UI Library for Modern Web Apps, powered by Vue & Tailwind CSS.
136 lines (135 loc) • 4.62 kB
JavaScript
import { ref, computed, toValue } from "vue";
import { useEventListener, useActiveElement, useDebounceFn } from "@vueuse/core";
const chainedShortcutRegex = /^[^-]+.*-.*[^-]+$/;
const combinedShortcutRegex = /^[^_]+.*_.*[^_]+$/;
export function extractShortcuts(items) {
const shortcuts = {};
function traverse(items2) {
items2.forEach((item) => {
if (item.kbds?.length && (item.onSelect || item.onClick)) {
const shortcutKey = item.kbds.join("_");
shortcuts[shortcutKey] = item.onSelect || item.onClick;
}
if (item.children) {
traverse(item.children.flat());
}
if (item.items) {
traverse(item.items.flat());
}
});
}
traverse(items.flat());
return shortcuts;
}
export function defineShortcuts(config, options = {}) {
const chainedInputs = ref([]);
const clearChainedInput = () => {
chainedInputs.value.splice(0, chainedInputs.value.length);
};
const debouncedClearChainedInput = useDebounceFn(clearChainedInput, options.chainDelay ?? 800);
const activeElement = useActiveElement();
const onKeyDown = (e) => {
if (!e.key) {
return;
}
const alphabeticalKey = /^[a-z]{1}$/i.test(e.key);
let chainedKey;
chainedInputs.value.push(e.key);
if (chainedInputs.value.length >= 2) {
chainedKey = chainedInputs.value.slice(-2).join("-");
for (const shortcut of shortcuts.value.filter((s) => s.chained)) {
if (shortcut.key !== chainedKey) {
continue;
}
if (shortcut.enabled) {
e.preventDefault();
shortcut.handler(e);
}
clearChainedInput();
return;
}
}
for (const shortcut of shortcuts.value.filter((s) => !s.chained)) {
if (e.key.toLowerCase() !== shortcut.key) {
continue;
}
if (e.metaKey !== shortcut.metaKey) {
continue;
}
if (e.ctrlKey !== shortcut.ctrlKey) {
continue;
}
if (alphabeticalKey && e.shiftKey !== shortcut.shiftKey) {
continue;
}
if (shortcut.enabled) {
e.preventDefault();
shortcut.handler();
}
clearChainedInput();
return;
}
debouncedClearChainedInput();
};
const usingInput = computed(() => {
const tagName = activeElement.value?.tagName;
const contentEditable = activeElement.value?.contentEditable;
const usingInput2 = !!(tagName === "INPUT" || tagName === "TEXTAREA" || contentEditable === "true" || contentEditable === "plaintext-only");
if (usingInput2) {
return activeElement.value?.name || true;
}
return false;
});
const shortcuts = computed(() => {
return Object.entries(toValue(config)).map(([key, shortcutConfig]) => {
if (!shortcutConfig) {
return null;
}
let shortcut;
if (key.includes("-") && key !== "-" && !key.match(chainedShortcutRegex)?.length) {
console.trace(`[Shortcut] Invalid key: "${key}"`);
}
if (key.includes("_") && key !== "_" && !key.match(combinedShortcutRegex)?.length) {
console.trace(`[Shortcut] Invalid key: "${key}"`);
}
const chained = key.includes("-") && key !== "-";
if (chained) {
shortcut = {
key: key.toLowerCase(),
metaKey: false,
ctrlKey: false,
shiftKey: false,
altKey: false
};
} else {
const keySplit = key.toLowerCase().split("_").map((k) => k);
shortcut = {
key: keySplit.filter((k) => !["meta", "command", "ctrl", "shift", "alt", "option"].includes(k)).join("_"),
metaKey: keySplit.includes("meta") || keySplit.includes("command"),
ctrlKey: keySplit.includes("ctrl"),
shiftKey: keySplit.includes("shift"),
altKey: keySplit.includes("alt") || keySplit.includes("option")
};
}
shortcut.chained = chained;
if (typeof shortcutConfig === "function") {
shortcut.handler = shortcutConfig;
} else if (typeof shortcutConfig === "object") {
shortcut = { ...shortcut, handler: shortcutConfig.handler };
}
if (!shortcut.handler) {
console.trace("[Shortcut] Invalid value");
return null;
}
let enabled = true;
if (!shortcutConfig.usingInput) {
enabled = !usingInput.value;
} else if (typeof shortcutConfig.usingInput === "string") {
enabled = usingInput.value === shortcutConfig.usingInput;
}
shortcut.enabled = enabled;
return shortcut;
}).filter(Boolean);
});
return useEventListener("keydown", onKeyDown);
}