Commit 3c3e640d69b48d8e9382acd25b60d906af038a9d
1 parent
819bcbe5
feat(hook): add useKeyPress
Showing
8 changed files
with
261 additions
and
82 deletions
src/components/Application/src/search/useMenuSearch.ts
1 | 1 | import { cloneDeep } from 'lodash-es'; |
2 | -import { ref, onBeforeUnmount, onBeforeMount, unref, Ref } from 'vue'; | |
2 | +import { ref, onBeforeMount, unref, Ref } from 'vue'; | |
3 | 3 | import { useI18n } from '/@/hooks/web/useI18n'; |
4 | 4 | import { getMenus } from '/@/router/menus'; |
5 | 5 | import type { Menu } from '/@/router/types'; |
... | ... | @@ -7,6 +7,7 @@ import { filter, forEach } from '/@/utils/helper/treeHelper'; |
7 | 7 | import { useDebounce } from '/@/hooks/core/useDebounce'; |
8 | 8 | import { useGo } from '/@/hooks/web/usePage'; |
9 | 9 | import { useScrollTo } from '/@/hooks/event/useScrollTo'; |
10 | +import { useKeyPress } from '/@/hooks/event/useKeyPress'; | |
10 | 11 | |
11 | 12 | export interface SearchResult { |
12 | 13 | name: string; |
... | ... | @@ -50,12 +51,6 @@ export function useMenuSearch(refs: Ref<HTMLElement[]>, scrollWrap: Ref<ElRef>, |
50 | 51 | forEach(menuList, (item) => { |
51 | 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 | 56 | function search(e: ChangeEvent) { |
... | ... | @@ -151,8 +146,8 @@ export function useMenuSearch(refs: Ref<HTMLElement[]>, scrollWrap: Ref<ElRef>, |
151 | 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 | 151 | switch (keyCode) { |
157 | 152 | case KeyCodeEnum.UP: |
158 | 153 | handleUp(); |
... | ... | @@ -167,7 +162,7 @@ export function useMenuSearch(refs: Ref<HTMLElement[]>, scrollWrap: Ref<ElRef>, |
167 | 162 | handleClose(); |
168 | 163 | break; |
169 | 164 | } |
170 | - } | |
165 | + }); | |
171 | 166 | |
172 | 167 | return { handleSearch, searchResult, keyword, activeIndex, handleMouseenter, handleEnter }; |
173 | 168 | } | ... | ... |
src/hooks/core/useEffect.ts
1 | -import { WatchOptions } from 'vue'; | |
2 | 1 | import { watch } from 'vue'; |
3 | 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 | 8 | return watch( |
7 | 9 | dependencies, |
8 | 10 | (changedDependencies, prevDependencies, onCleanUp) => { |
... | ... | @@ -11,6 +13,6 @@ export const useEffect = (effectHandler: Fn, dependencies: any[]) => { |
11 | 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