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 | 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[]) => { | @@ -11,6 +13,6 @@ export const useEffect = (effectHandler: Fn, dependencies: any[]) => { | ||
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) { |