Commit 3c3e640d69b48d8e9382acd25b60d906af038a9d

Authored by vben
1 parent 819bcbe5

feat(hook): add useKeyPress

src/components/Application/src/search/useMenuSearch.ts
1 import { cloneDeep } from 'lodash-es'; 1 import { cloneDeep } from 'lodash-es';
2 -import { ref, onBeforeUnmount, onBeforeMount, unref, Ref } from 'vue'; 2 +import { ref, onBeforeMount, unref, Ref } from 'vue';
3 import { useI18n } from '/@/hooks/web/useI18n'; 3 import { useI18n } from '/@/hooks/web/useI18n';
4 import { getMenus } from '/@/router/menus'; 4 import { getMenus } from '/@/router/menus';
5 import type { Menu } from '/@/router/types'; 5 import type { Menu } from '/@/router/types';
@@ -7,6 +7,7 @@ import { filter, forEach } from '/@/utils/helper/treeHelper'; @@ -7,6 +7,7 @@ import { filter, forEach } from '/@/utils/helper/treeHelper';
7 import { useDebounce } from '/@/hooks/core/useDebounce'; 7 import { useDebounce } from '/@/hooks/core/useDebounce';
8 import { useGo } from '/@/hooks/web/usePage'; 8 import { useGo } from '/@/hooks/web/usePage';
9 import { useScrollTo } from '/@/hooks/event/useScrollTo'; 9 import { useScrollTo } from '/@/hooks/event/useScrollTo';
  10 +import { useKeyPress } from '/@/hooks/event/useKeyPress';
10 11
11 export interface SearchResult { 12 export interface SearchResult {
12 name: string; 13 name: string;
@@ -50,12 +51,6 @@ export function useMenuSearch(refs: Ref<HTMLElement[]>, scrollWrap: Ref<ElRef>, @@ -50,12 +51,6 @@ export function useMenuSearch(refs: Ref<HTMLElement[]>, scrollWrap: Ref<ElRef>,
50 forEach(menuList, (item) => { 51 forEach(menuList, (item) => {
51 item.name = t(item.name); 52 item.name = t(item.name);
52 }); 53 });
53 -  
54 - document.addEventListener('keydown', registerKeyDown);  
55 - });  
56 -  
57 - onBeforeUnmount(() => {  
58 - document.removeEventListener('keydown', registerKeyDown);  
59 }); 54 });
60 55
61 function search(e: ChangeEvent) { 56 function search(e: ChangeEvent) {
@@ -151,8 +146,8 @@ export function useMenuSearch(refs: Ref<HTMLElement[]>, scrollWrap: Ref<ElRef>, @@ -151,8 +146,8 @@ export function useMenuSearch(refs: Ref<HTMLElement[]>, scrollWrap: Ref<ElRef>,
151 emit('close'); 146 emit('close');
152 } 147 }
153 148
154 - function registerKeyDown(e: KeyboardEvent) {  
155 - const keyCode = window.event ? e.keyCode : e.which; 149 + useKeyPress(['enter', 'up', 'down'], (events) => {
  150 + const keyCode = events.keyCode;
156 switch (keyCode) { 151 switch (keyCode) {
157 case KeyCodeEnum.UP: 152 case KeyCodeEnum.UP:
158 handleUp(); 153 handleUp();
@@ -167,7 +162,7 @@ export function useMenuSearch(refs: Ref<HTMLElement[]>, scrollWrap: Ref<ElRef>, @@ -167,7 +162,7 @@ export function useMenuSearch(refs: Ref<HTMLElement[]>, scrollWrap: Ref<ElRef>,
167 handleClose(); 162 handleClose();
168 break; 163 break;
169 } 164 }
170 - } 165 + });
171 166
172 return { handleSearch, searchResult, keyword, activeIndex, handleMouseenter, handleEnter }; 167 return { handleSearch, searchResult, keyword, activeIndex, handleMouseenter, handleEnter };
173 } 168 }
src/hooks/core/useEffect.ts
1 -import { WatchOptions } from 'vue';  
2 import { watch } from 'vue'; 1 import { watch } from 'vue';
3 import { isFunction } from '/@/utils/is'; 2 import { isFunction } from '/@/utils/is';
4 3
5 -export const useEffect = (effectHandler: Fn, dependencies: any[]) => { 4 +export function useEffect<T extends any = any>(
  5 + effectHandler: (deps: T[], prevDeps?: T[]) => () => void,
  6 + dependencies: T[]
  7 +) {
6 return watch( 8 return watch(
7 dependencies, 9 dependencies,
8 (changedDependencies, prevDependencies, onCleanUp) => { 10 (changedDependencies, prevDependencies, onCleanUp) => {
@@ -11,6 +13,6 @@ export const useEffect = (effectHandler: Fn, dependencies: any[]) =&gt; { @@ -11,6 +13,6 @@ export const useEffect = (effectHandler: Fn, dependencies: any[]) =&gt; {
11 onCleanUp(effectCleaner); 13 onCleanUp(effectCleaner);
12 } 14 }
13 }, 15 },
14 - { immediate: true, deep: true } as WatchOptions 16 + { immediate: true, deep: true }
15 ); 17 );
16 -}; 18 +}
src/hooks/core/useLockFn.ts 0 → 100644
  1 +import { ref, unref } from 'vue';
  2 +
  3 +export function useLockFn<P extends any[] = any[], V extends any = any>(
  4 + fn: (...args: P) => Promise<V>
  5 +) {
  6 + const lockRef = ref(false);
  7 + return async function (...args: P) {
  8 + if (unref(lockRef)) return;
  9 + lockRef.value = true;
  10 + try {
  11 + const ret = await fn(...args);
  12 + lockRef.value = false;
  13 + return ret;
  14 + } catch (e) {
  15 + lockRef.value = false;
  16 + throw e;
  17 + }
  18 + };
  19 +}
src/hooks/core/useModel.ts deleted 100644 → 0
1 -import { toRef, Ref, reactive, customRef, SetupContext, watch, UnwrapRef } from 'vue';  
2 -  
3 -export type ModelProps<U> = Readonly<  
4 - { [props: string]: any } & {  
5 - modelValue?: U;  
6 - }  
7 ->;  
8 -  
9 -export function useModel<T>(  
10 - props: ModelProps<T>,  
11 - context: SetupContext,  
12 - callback?: (val: T | undefined, internalState: { value: UnwrapRef<T | undefined> }) => any  
13 -) {  
14 - const outerModel: Ref<T | undefined> = toRef(props, 'modelValue');  
15 - const internalState = reactive({  
16 - value: props.modelValue,  
17 - });  
18 -  
19 - const internalModel = customRef<UnwrapRef<T> | undefined>((track, trigger) => {  
20 - return {  
21 - get() {  
22 - track();  
23 - return internalState.value;  
24 - },  
25 - set(newVal) {  
26 - if (internalState.value === newVal) return;  
27 - internalState.value = newVal;  
28 - context.emit('update:modelValue', newVal);  
29 - trigger();  
30 - },  
31 - };  
32 - });  
33 -  
34 - watch(outerModel, (val, oldVal) => {  
35 - if (val === oldVal || val === internalState.value) return;  
36 - if (callback) {  
37 - callback(val, internalState);  
38 - return;  
39 - }  
40 - internalState.value = val as UnwrapRef<T> | undefined;  
41 - });  
42 -  
43 - return {  
44 - internalState,  
45 - internalModel,  
46 - };  
47 -}  
src/hooks/core/useState.ts 0 → 100644
  1 +import { isObject } from '@vue/shared';
  2 +import { reactive, Ref, ref, readonly } from 'vue';
  3 +import { isFunction } from '/@/utils/is';
  4 +
  5 +type State<T> = ((s: T) => T) | T;
  6 +type Dispatch<T> = (t: T) => void;
  7 +
  8 +type DispatchState<T> = Dispatch<State<T>>;
  9 +
  10 +type ResultState<T> = Readonly<Ref<T>>;
  11 +
  12 +export function useState<T extends undefined>(
  13 + initialState: (() => T) | T
  14 +): [ResultState<T>, DispatchState<T>];
  15 +
  16 +export function useState<T extends null>(
  17 + initialState: (() => T) | T
  18 +): [ResultState<T>, DispatchState<T>];
  19 +
  20 +export function useState<T extends boolean>(
  21 + initialState: (() => T) | T
  22 +): [ResultState<boolean>, DispatchState<boolean>];
  23 +
  24 +export function useState<T extends string>(
  25 + initialState: (() => T) | T
  26 +): [ResultState<string>, DispatchState<string>];
  27 +
  28 +export function useState<T extends number>(
  29 + initialState: (() => T) | T
  30 +): [ResultState<number>, DispatchState<number>];
  31 +
  32 +export function useState<T extends object>(
  33 + initialState: (() => T) | T
  34 +): [Readonly<T>, DispatchState<T>];
  35 +
  36 +export function useState<T extends any>(
  37 + initialState: (() => T) | T
  38 +): [Readonly<T>, DispatchState<T>];
  39 +
  40 +export function useState<T>(initialState: (() => T) | T): [ResultState<T> | T, DispatchState<T>] {
  41 + if (isFunction(initialState)) {
  42 + initialState = (initialState as Fn)();
  43 + }
  44 +
  45 + if (isObject(initialState)) {
  46 + const state = reactive({ data: initialState }) as any;
  47 + const setState = (newState: T) => {
  48 + state.data = newState;
  49 + };
  50 + return [readonly(state), setState];
  51 + } else {
  52 + const state = ref(initialState) as any;
  53 + const setState = (newState: T) => {
  54 + state.value = newState;
  55 + };
  56 + return [readonly(state), setState];
  57 + }
  58 +}
src/hooks/core/useToggle.ts deleted 100644 → 0
1 -import { ref, watch, Ref, SetupContext } from 'vue';  
2 -  
3 -export function useToggle(internalModel: Ref<unknown>, { emit }: SetupContext) {  
4 - const isActive = ref(!!internalModel.value);  
5 - const isToggled = ref(false);  
6 - watch(internalModel, (val) => {  
7 - isActive.value = !!val;  
8 - });  
9 - watch(isActive, (value) => {  
10 - !!value !== !!internalModel.value && emit('onUpdate:modelValue', value);  
11 - });  
12 - function toggleIt() {  
13 - isToggled.value = !isToggled.value;  
14 - }  
15 - return {  
16 - isActive,  
17 - toggleIt,  
18 - isToggled,  
19 - };  
20 -}  
src/hooks/event/useKeyPress.ts 0 → 100644
  1 +// https://ahooks.js.org/zh-CN/hooks/dom/use-key-press
  2 +
  3 +import type { Ref } from 'vue';
  4 +import { onBeforeUnmount, onMounted, unref } from 'vue';
  5 +import { noop } from '/@/utils';
  6 +import { isFunction, isString, isNumber, isArray } from '/@/utils/is';
  7 +
  8 +export type KeyPredicate = (event: KeyboardEvent) => boolean;
  9 +export type keyType = KeyboardEvent['keyCode'] | KeyboardEvent['key'];
  10 +export type KeyFilter = keyType | keyType[] | ((event: KeyboardEvent) => boolean);
  11 +export type EventHandler = (event: KeyboardEvent) => void;
  12 +
  13 +export type keyEvent = 'keydown' | 'keyup';
  14 +
  15 +export type TargetElement = HTMLElement | Element | Document | Window;
  16 +export type Target = Ref<TargetElement>;
  17 +
  18 +export type EventOption = {
  19 + events?: keyEvent[];
  20 + target?: Target;
  21 +};
  22 +
  23 +const defaultEvents: keyEvent[] = ['keydown'];
  24 +
  25 +// 键盘事件 keyCode 别名
  26 +const aliasKeyCodeMap: Record<string, number | number[]> = {
  27 + esc: 27,
  28 + tab: 9,
  29 + enter: 13,
  30 + space: 32,
  31 + up: 38,
  32 + left: 37,
  33 + right: 39,
  34 + down: 40,
  35 + delete: [8, 46],
  36 +};
  37 +
  38 +// 键盘事件 key 别名
  39 +const aliasKeyMap: Record<string, string | string[]> = {
  40 + esc: 'Escape',
  41 + tab: 'Tab',
  42 + enter: 'Enter',
  43 + space: ' ',
  44 + // IE11 uses key names without `Arrow` prefix for arrow keys.
  45 + up: ['Up', 'ArrowUp'],
  46 + left: ['Left', 'ArrowLeft'],
  47 + right: ['Right', 'ArrowRight'],
  48 + down: ['Down', 'ArrowDown'],
  49 + delete: ['Backspace', 'Delete'],
  50 +};
  51 +
  52 +// 修饰键
  53 +const modifierKey: Record<string, (event: KeyboardEvent) => boolean> = {
  54 + ctrl: (event: KeyboardEvent) => event.ctrlKey,
  55 + shift: (event: KeyboardEvent) => event.shiftKey,
  56 + alt: (event: KeyboardEvent) => event.altKey,
  57 + meta: (event: KeyboardEvent) => event.metaKey,
  58 +};
  59 +
  60 +/**
  61 + * 判断按键是否激活
  62 + * @param [event: KeyboardEvent]键盘事件
  63 + * @param [keyFilter: any] 当前键
  64 + * @returns Boolean
  65 + */
  66 +function genFilterKey(event: any, keyFilter: any) {
  67 + // 浏览器自动补全 input 的时候,会触发 keyDown、keyUp 事件,但此时 event.key 等为空
  68 + if (!event.key) {
  69 + return false;
  70 + }
  71 +
  72 + // 数字类型直接匹配事件的 keyCode
  73 + if (isNumber(keyFilter)) {
  74 + return event.keyCode === keyFilter;
  75 + }
  76 + // 字符串依次判断是否有组合键
  77 + const genArr = keyFilter.split('.');
  78 + let genLen = 0;
  79 + for (const key of genArr) {
  80 + // 组合键
  81 + const genModifier = modifierKey[key];
  82 + // key 别名
  83 + const aliasKey = aliasKeyMap[key];
  84 + // keyCode 别名
  85 + const aliasKeyCode = aliasKeyCodeMap[key];
  86 + /**
  87 + * 满足以上规则
  88 + * 1. 自定义组合键别名
  89 + * 2. 自定义 key 别名
  90 + * 3. 自定义 keyCode 别名
  91 + * 4. 匹配 key 或 keyCode
  92 + */
  93 + if (
  94 + (genModifier && genModifier(event)) ||
  95 + (aliasKey && isArray(aliasKey) ? aliasKey.includes(event.key) : aliasKey === event.key) ||
  96 + (aliasKeyCode && isArray(aliasKeyCode)
  97 + ? aliasKeyCode.includes(event.keyCode)
  98 + : aliasKeyCode === event.keyCode) ||
  99 + event.key.toUpperCase() === key.toUpperCase()
  100 + ) {
  101 + genLen++;
  102 + }
  103 + }
  104 + return genLen === genArr.length;
  105 +}
  106 +
  107 +/**
  108 + * 键盘输入预处理方法
  109 + */
  110 +function genKeyFormat(keyFilter: any): KeyPredicate {
  111 + if (isFunction(keyFilter)) {
  112 + return keyFilter;
  113 + }
  114 + if (isString(keyFilter) || isNumber(keyFilter)) {
  115 + return (event: KeyboardEvent) => genFilterKey(event, keyFilter);
  116 + }
  117 + if (isArray(keyFilter)) {
  118 + return (event: KeyboardEvent) => keyFilter.some((item: any) => genFilterKey(event, item));
  119 + }
  120 + return keyFilter ? () => true : () => false;
  121 +}
  122 +
  123 +export function useKeyPress(
  124 + keyFilter: KeyFilter,
  125 + eventHandler: EventHandler = noop,
  126 + option: EventOption = {}
  127 +) {
  128 + const { events = defaultEvents, target } = option;
  129 +
  130 + let el: TargetElement | null | undefined;
  131 +
  132 + function handler(event: any) {
  133 + const genGuard: KeyPredicate = genKeyFormat(keyFilter);
  134 + if (genGuard(event)) {
  135 + return eventHandler(event);
  136 + }
  137 + }
  138 +
  139 + onMounted(() => {
  140 + el = getTargetElement(target, window);
  141 + if (!el) return;
  142 +
  143 + for (const eventName of events) {
  144 + el.addEventListener(eventName, handler);
  145 + }
  146 + });
  147 +
  148 + onBeforeUnmount(() => {
  149 + if (!el) return;
  150 + for (const eventName of events) {
  151 + el.removeEventListener(eventName, handler);
  152 + }
  153 + });
  154 +}
  155 +
  156 +export function getTargetElement(
  157 + target?: Target,
  158 + defaultElement?: TargetElement
  159 +): TargetElement | undefined | null {
  160 + if (!target) {
  161 + return defaultElement;
  162 + }
  163 +
  164 + let targetElement: TargetElement | undefined | null;
  165 +
  166 + if (isFunction(target)) {
  167 + targetElement = target();
  168 + } else {
  169 + targetElement = unref(target);
  170 + }
  171 + return targetElement;
  172 +}
src/hooks/web/useScript.ts
@@ -16,7 +16,7 @@ export function useScript(opts: ScriptOptions) { @@ -16,7 +16,7 @@ export function useScript(opts: ScriptOptions) {
16 isLoading.value = false; 16 isLoading.value = false;
17 success.value = true; 17 success.value = true;
18 error.value = false; 18 error.value = false;
19 - resolve(); 19 + resolve('');
20 }; 20 };
21 21
22 script.onerror = function (err) { 22 script.onerror = function (err) {