ui-thing
Version:
CLI used to add Nuxt components to a project
316 lines (268 loc) • 8.22 kB
text/typescript
export const DEFINE_SHORTCUT = `import type { MaybeRef } from "vue";
type KbdKeysSpecificMap = {
meta: string;
alt: string;
ctrl: string;
};
export const kbdKeysMap = {
meta: "",
ctrl: "",
alt: "",
win: "⊞",
command: "⌘",
shift: "⇧",
control: "⌃",
option: "⌥",
enter: "↵",
delete: "⌦",
backspace: "⌫",
escape: "⎋",
tab: "⇥",
capslock: "⇪",
arrowup: "↑",
arrowright: "→",
arrowdown: "↓",
arrowleft: "←",
pageup: "⇞",
pagedown: "⇟",
home: "↖",
end: "↘",
};
export type KbdKey = keyof typeof kbdKeysMap;
export type KbdKeySpecific = keyof KbdKeysSpecificMap;
const _useKbd = () => {
const macOS = computed(
() =>
import.meta.client &&
navigator &&
navigator.userAgent &&
navigator.userAgent.match(/Macintosh;/)
);
const kbdKeysSpecificMap = reactive({
meta: " ",
alt: " ",
ctrl: " ",
});
onMounted(() => {
kbdKeysSpecificMap.meta = macOS.value ? kbdKeysMap.command : "Ctrl";
kbdKeysSpecificMap.ctrl = macOS.value ? kbdKeysMap.control : "Ctrl";
kbdKeysSpecificMap.alt = macOS.value ? kbdKeysMap.option : "Alt";
});
function getKbdKey(value?: KbdKey | string) {
if (!value) {
return;
}
if (["meta", "alt", "ctrl"].includes(value)) {
return kbdKeysSpecificMap[value as KbdKeySpecific];
}
return kbdKeysMap[value as KbdKey] || value.toUpperCase();
}
return {
macOS,
getKbdKey,
};
};
export const useKbd = /* @__PURE__ */ createSharedComposable(_useKbd);
type Handler = (e?: any) => void;
export interface ShortcutConfig {
handler: Handler;
usingInput?: string | boolean;
}
export interface ShortcutsConfig {
[key: string]: ShortcutConfig | Handler | false | null | undefined;
}
export interface ShortcutsOptions {
chainDelay?: number;
}
interface Shortcut {
handler: Handler;
enabled: boolean;
chained: boolean;
// KeyboardEvent attributes
key: string;
ctrlKey: boolean;
metaKey: boolean;
shiftKey: boolean;
altKey: boolean;
// code?: string
// keyCode?: number
}
const chainedShortcutRegex = /^[^-]+.*-.*[^-]+$/;
const combinedShortcutRegex = /^[^_]+.*_.*[^_]+$/;
// keyboard keys which can be combined with Shift modifier (in addition to alphabet keys)
const shiftableKeys = [
"arrowleft",
"arrowright",
"arrowup",
"arrowright",
"tab",
"escape",
"enter",
"backspace",
];
export function extractShortcuts(items: any[] | any[][]) {
const shortcuts: Record<string, Handler> = {};
function traverse(items: any[]) {
items.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: MaybeRef<ShortcutsConfig>, options: ShortcutsOptions = {}) {
const chainedInputs = ref<string[]>([]);
const clearChainedInput = () => {
chainedInputs.value.splice(0, chainedInputs.value.length);
};
const debouncedClearChainedInput = useDebounceFn(clearChainedInput, options.chainDelay ?? 800);
const { macOS } = useKbd();
const activeElement = useActiveElement();
const onKeyDown = (e: KeyboardEvent) => {
// Input autocomplete triggers a keydown event
if (!e.key) {
return;
}
const alphabetKey = /^[a-z]{1}$/i.test(e.key);
const shiftableKey = shiftableKeys.includes(e.key.toLowerCase());
let chainedKey;
chainedInputs.value.push(e.key);
// try matching a chained shortcut
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;
}
}
// try matching a standard shortcut
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;
}
// shift modifier is only checked in combination with alphabet keys and some extra keys
// (shift with special characters would change the key)
if ((alphabetKey || shiftableKey) && e.shiftKey !== shortcut.shiftKey) {
continue;
}
// alt modifier changes the combined key anyways
// if (e.altKey !== shortcut.altKey) { continue }
if (shortcut.enabled) {
e.preventDefault();
shortcut.handler(e);
}
clearChainedInput();
return;
}
debouncedClearChainedInput();
};
const usingInput = computed(() => {
const tagName = activeElement.value?.tagName;
const contentEditable = activeElement.value?.contentEditable;
const usingInput = !!(
tagName === "INPUT" ||
tagName === "TEXTAREA" ||
contentEditable === "true" ||
contentEditable === "plaintext-only"
);
if (usingInput) {
return ((activeElement.value as any)?.name as string) || true;
}
return false;
});
// Map config to full detailed shortcuts
const shortcuts = computed<Shortcut[]>(() => {
return Object.entries(toValue(config))
.map(([key, shortcutConfig]) => {
if (!shortcutConfig) {
return null;
}
// Parse key and modifiers
let shortcut: Partial<Shortcut>;
if (
key.includes("-") &&
key !== "-" &&
!key.includes("_") &&
!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 !== "-" && !key.includes("_");
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;
// Convert Meta to Ctrl for non-MacOS
if (!macOS.value && shortcut.metaKey && !shortcut.ctrlKey) {
shortcut.metaKey = false;
shortcut.ctrlKey = true;
}
// Retrieve handler function
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 as ShortcutConfig).usingInput) {
enabled = !usingInput.value;
} else if (typeof (shortcutConfig as ShortcutConfig).usingInput === "string") {
enabled = usingInput.value === (shortcutConfig as ShortcutConfig).usingInput;
}
shortcut.enabled = enabled;
return shortcut;
})
.filter(Boolean) as Shortcut[];
});
return useEventListener("keydown", onKeyDown);
}
`;