ui-thing
Version:
CLI used to add Nuxt 3 components to a project
223 lines (187 loc) • 5.94 kB
text/typescript
export const DEFINE_SHORTCUT = `/* eslint-disable @typescript-eslint/no-unsafe-function-type */
import { logicAnd, logicNot } from "@vueuse/math";
import type { ComputedRef, WatchSource } from "vue";
export interface ShortcutConfig {
handler: Function;
usingInput?: string | boolean;
whenever?: WatchSource<boolean>[];
}
export interface ShortcutsConfig {
[key: string]: ShortcutConfig | Function;
}
export interface ShortcutsOptions {
chainDelay?: number;
}
interface Shortcut {
handler: Function;
condition: ComputedRef<boolean>;
chained: boolean;
// KeyboardEvent attributes
key: string;
ctrlKey: boolean;
metaKey: boolean;
shiftKey: boolean;
altKey: boolean;
// code?: string
// keyCode?: number
}
export const defineShortcuts = (config: ShortcutsConfig, options: ShortcutsOptions = {}) => {
const { macOS, usingInput } = useShortcuts();
let shortcuts: Shortcut[] = [];
const chainedInputs = ref<any[]>([]);
const clearChainedInput = () => {
chainedInputs.value.splice(0, chainedInputs.value.length);
};
const debouncedClearChainedInput = useDebounceFn(clearChainedInput, options.chainDelay ?? 800);
const onKeyDown = (e: KeyboardEvent) => {
// Input autocomplete triggers a keydown event
if (!e.key) {
return;
}
const alphabeticalKey = /^[a-z]{1}$/i.test(e.key);
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.filter((s) => s.chained)) {
if (shortcut.key !== chainedKey) {
continue;
}
if (shortcut.condition.value) {
e.preventDefault();
shortcut.handler();
}
clearChainedInput();
return;
}
}
// try matching a standard shortcut
for (const shortcut of shortcuts.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 alphabetical keys
// (shift with non-alphabetical keys would change the key)
if (alphabeticalKey && e.shiftKey !== shortcut.shiftKey) {
continue;
}
// alt modifier changes the combined key anyways
// if (e.altKey !== shortcut.altKey) { continue }
if (shortcut.condition.value) {
e.preventDefault();
shortcut.handler();
}
clearChainedInput();
return;
}
debouncedClearChainedInput();
};
// Map config to full detailled shortcuts
shortcuts = Object.entries(config)
.map(([key, shortcutConfig]) => {
if (!shortcutConfig) {
return null;
}
// Parse key and modifiers
let shortcut: Partial<Shortcut>;
if (key.includes("-") && key.includes("_")) {
console.trace("[Shortcut] Invalid key");
return null;
}
const chained = 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", "ctrl", "shift", "alt"].includes(k)).join("_"),
metaKey: keySplit.includes("meta"),
ctrlKey: keySplit.includes("ctrl"),
shiftKey: keySplit.includes("shift"),
altKey: keySplit.includes("alt"),
};
}
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;
}
// Create shortcut computed
const conditions: ComputedRef<boolean>[] = [];
if (!(shortcutConfig as ShortcutConfig).usingInput) {
conditions.push(logicNot(usingInput));
} else if (typeof (shortcutConfig as ShortcutConfig).usingInput === "string") {
conditions.push(
computed(() => usingInput.value === (shortcutConfig as ShortcutConfig).usingInput)
);
}
shortcut.condition = logicAnd(
...conditions,
...((shortcutConfig as ShortcutConfig).whenever || [])
);
return shortcut as Shortcut;
})
.filter(Boolean) as Shortcut[];
useEventListener("keydown", onKeyDown);
};
`;
export const USE_SHORTCUTS = `export const _useShortcuts = () => {
const macOS = computed(
() =>
import.meta.client &&
navigator &&
navigator.userAgent &&
navigator.userAgent.match(/Macintosh;/)
);
const metaSymbol = ref(" ");
const activeElement = useActiveElement();
const usingInput = computed(() => {
const usingInput = !!(
activeElement.value?.tagName === "INPUT" ||
activeElement.value?.tagName === "TEXTAREA" ||
activeElement.value?.contentEditable === "true"
);
if (usingInput) {
return ((activeElement.value as any)?.name as string) || true;
}
return false;
});
onMounted(() => {
metaSymbol.value = macOS.value ? "⌘" : "Ctrl";
});
return {
macOS,
metaSymbol,
activeElement,
usingInput,
};
};
export const useShortcuts = createSharedComposable(_useShortcuts);
`;