UNPKG

@nuxt/ui

Version:

A UI Library for Modern Web Apps, powered by Vue & Tailwind CSS.

136 lines (135 loc) 4.62 kB
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); }