Browse Source

feat(hook): add useKeyPress

vben 4 years ago
parent
commit
3c3e640d69

+ 5 - 10
src/components/Application/src/search/useMenuSearch.ts

@@ -1,5 +1,5 @@
 import { cloneDeep } from 'lodash-es';
-import { ref, onBeforeUnmount, onBeforeMount, unref, Ref } from 'vue';
+import { ref, onBeforeMount, unref, Ref } from 'vue';
 import { useI18n } from '/@/hooks/web/useI18n';
 import { getMenus } from '/@/router/menus';
 import type { Menu } from '/@/router/types';
@@ -7,6 +7,7 @@ import { filter, forEach } from '/@/utils/helper/treeHelper';
 import { useDebounce } from '/@/hooks/core/useDebounce';
 import { useGo } from '/@/hooks/web/usePage';
 import { useScrollTo } from '/@/hooks/event/useScrollTo';
+import { useKeyPress } from '/@/hooks/event/useKeyPress';
 
 export interface SearchResult {
   name: string;
@@ -50,12 +51,6 @@ export function useMenuSearch(refs: Ref<HTMLElement[]>, scrollWrap: Ref<ElRef>,
     forEach(menuList, (item) => {
       item.name = t(item.name);
     });
-
-    document.addEventListener('keydown', registerKeyDown);
-  });
-
-  onBeforeUnmount(() => {
-    document.removeEventListener('keydown', registerKeyDown);
   });
 
   function search(e: ChangeEvent) {
@@ -151,8 +146,8 @@ export function useMenuSearch(refs: Ref<HTMLElement[]>, scrollWrap: Ref<ElRef>,
     emit('close');
   }
 
-  function registerKeyDown(e: KeyboardEvent) {
-    const keyCode = window.event ? e.keyCode : e.which;
+  useKeyPress(['enter', 'up', 'down'], (events) => {
+    const keyCode = events.keyCode;
     switch (keyCode) {
       case KeyCodeEnum.UP:
         handleUp();
@@ -167,7 +162,7 @@ export function useMenuSearch(refs: Ref<HTMLElement[]>, scrollWrap: Ref<ElRef>,
         handleClose();
         break;
     }
-  }
+  });
 
   return { handleSearch, searchResult, keyword, activeIndex, handleMouseenter, handleEnter };
 }

+ 6 - 4
src/hooks/core/useEffect.ts

@@ -1,8 +1,10 @@
-import { WatchOptions } from 'vue';
 import { watch } from 'vue';
 import { isFunction } from '/@/utils/is';
 
-export const useEffect = (effectHandler: Fn, dependencies: any[]) => {
+export function useEffect<T extends any = any>(
+  effectHandler: (deps: T[], prevDeps?: T[]) => () => void,
+  dependencies: T[]
+) {
   return watch(
     dependencies,
     (changedDependencies, prevDependencies, onCleanUp) => {
@@ -11,6 +13,6 @@ export const useEffect = (effectHandler: Fn, dependencies: any[]) => {
         onCleanUp(effectCleaner);
       }
     },
-    { immediate: true, deep: true } as WatchOptions
+    { immediate: true, deep: true }
   );
-};
+}

+ 19 - 0
src/hooks/core/useLockFn.ts

@@ -0,0 +1,19 @@
+import { ref, unref } from 'vue';
+
+export function useLockFn<P extends any[] = any[], V extends any = any>(
+  fn: (...args: P) => Promise<V>
+) {
+  const lockRef = ref(false);
+  return async function (...args: P) {
+    if (unref(lockRef)) return;
+    lockRef.value = true;
+    try {
+      const ret = await fn(...args);
+      lockRef.value = false;
+      return ret;
+    } catch (e) {
+      lockRef.value = false;
+      throw e;
+    }
+  };
+}

+ 0 - 47
src/hooks/core/useModel.ts

@@ -1,47 +0,0 @@
-import { toRef, Ref, reactive, customRef, SetupContext, watch, UnwrapRef } from 'vue';
-
-export type ModelProps<U> = Readonly<
-  { [props: string]: any } & {
-    modelValue?: U;
-  }
->;
-
-export function useModel<T>(
-  props: ModelProps<T>,
-  context: SetupContext,
-  callback?: (val: T | undefined, internalState: { value: UnwrapRef<T | undefined> }) => any
-) {
-  const outerModel: Ref<T | undefined> = toRef(props, 'modelValue');
-  const internalState = reactive({
-    value: props.modelValue,
-  });
-
-  const internalModel = customRef<UnwrapRef<T> | undefined>((track, trigger) => {
-    return {
-      get() {
-        track();
-        return internalState.value;
-      },
-      set(newVal) {
-        if (internalState.value === newVal) return;
-        internalState.value = newVal;
-        context.emit('update:modelValue', newVal);
-        trigger();
-      },
-    };
-  });
-
-  watch(outerModel, (val, oldVal) => {
-    if (val === oldVal || val === internalState.value) return;
-    if (callback) {
-      callback(val, internalState);
-      return;
-    }
-    internalState.value = val as UnwrapRef<T> | undefined;
-  });
-
-  return {
-    internalState,
-    internalModel,
-  };
-}

+ 58 - 0
src/hooks/core/useState.ts

@@ -0,0 +1,58 @@
+import { isObject } from '@vue/shared';
+import { reactive, Ref, ref, readonly } from 'vue';
+import { isFunction } from '/@/utils/is';
+
+type State<T> = ((s: T) => T) | T;
+type Dispatch<T> = (t: T) => void;
+
+type DispatchState<T> = Dispatch<State<T>>;
+
+type ResultState<T> = Readonly<Ref<T>>;
+
+export function useState<T extends undefined>(
+  initialState: (() => T) | T
+): [ResultState<T>, DispatchState<T>];
+
+export function useState<T extends null>(
+  initialState: (() => T) | T
+): [ResultState<T>, DispatchState<T>];
+
+export function useState<T extends boolean>(
+  initialState: (() => T) | T
+): [ResultState<boolean>, DispatchState<boolean>];
+
+export function useState<T extends string>(
+  initialState: (() => T) | T
+): [ResultState<string>, DispatchState<string>];
+
+export function useState<T extends number>(
+  initialState: (() => T) | T
+): [ResultState<number>, DispatchState<number>];
+
+export function useState<T extends object>(
+  initialState: (() => T) | T
+): [Readonly<T>, DispatchState<T>];
+
+export function useState<T extends any>(
+  initialState: (() => T) | T
+): [Readonly<T>, DispatchState<T>];
+
+export function useState<T>(initialState: (() => T) | T): [ResultState<T> | T, DispatchState<T>] {
+  if (isFunction(initialState)) {
+    initialState = (initialState as Fn)();
+  }
+
+  if (isObject(initialState)) {
+    const state = reactive({ data: initialState }) as any;
+    const setState = (newState: T) => {
+      state.data = newState;
+    };
+    return [readonly(state), setState];
+  } else {
+    const state = ref(initialState) as any;
+    const setState = (newState: T) => {
+      state.value = newState;
+    };
+    return [readonly(state), setState];
+  }
+}

+ 0 - 20
src/hooks/core/useToggle.ts

@@ -1,20 +0,0 @@
-import { ref, watch, Ref, SetupContext } from 'vue';
-
-export function useToggle(internalModel: Ref<unknown>, { emit }: SetupContext) {
-  const isActive = ref(!!internalModel.value);
-  const isToggled = ref(false);
-  watch(internalModel, (val) => {
-    isActive.value = !!val;
-  });
-  watch(isActive, (value) => {
-    !!value !== !!internalModel.value && emit('onUpdate:modelValue', value);
-  });
-  function toggleIt() {
-    isToggled.value = !isToggled.value;
-  }
-  return {
-    isActive,
-    toggleIt,
-    isToggled,
-  };
-}

+ 172 - 0
src/hooks/event/useKeyPress.ts

@@ -0,0 +1,172 @@
+// https://ahooks.js.org/zh-CN/hooks/dom/use-key-press
+
+import type { Ref } from 'vue';
+import { onBeforeUnmount, onMounted, unref } from 'vue';
+import { noop } from '/@/utils';
+import { isFunction, isString, isNumber, isArray } from '/@/utils/is';
+
+export type KeyPredicate = (event: KeyboardEvent) => boolean;
+export type keyType = KeyboardEvent['keyCode'] | KeyboardEvent['key'];
+export type KeyFilter = keyType | keyType[] | ((event: KeyboardEvent) => boolean);
+export type EventHandler = (event: KeyboardEvent) => void;
+
+export type keyEvent = 'keydown' | 'keyup';
+
+export type TargetElement = HTMLElement | Element | Document | Window;
+export type Target = Ref<TargetElement>;
+
+export type EventOption = {
+  events?: keyEvent[];
+  target?: Target;
+};
+
+const defaultEvents: keyEvent[] = ['keydown'];
+
+// 键盘事件 keyCode 别名
+const aliasKeyCodeMap: Record<string, number | number[]> = {
+  esc: 27,
+  tab: 9,
+  enter: 13,
+  space: 32,
+  up: 38,
+  left: 37,
+  right: 39,
+  down: 40,
+  delete: [8, 46],
+};
+
+// 键盘事件 key 别名
+const aliasKeyMap: Record<string, string | string[]> = {
+  esc: 'Escape',
+  tab: 'Tab',
+  enter: 'Enter',
+  space: ' ',
+  // IE11 uses key names without `Arrow` prefix for arrow keys.
+  up: ['Up', 'ArrowUp'],
+  left: ['Left', 'ArrowLeft'],
+  right: ['Right', 'ArrowRight'],
+  down: ['Down', 'ArrowDown'],
+  delete: ['Backspace', 'Delete'],
+};
+
+// 修饰键
+const modifierKey: Record<string, (event: KeyboardEvent) => boolean> = {
+  ctrl: (event: KeyboardEvent) => event.ctrlKey,
+  shift: (event: KeyboardEvent) => event.shiftKey,
+  alt: (event: KeyboardEvent) => event.altKey,
+  meta: (event: KeyboardEvent) => event.metaKey,
+};
+
+/**
+ * 判断按键是否激活
+ * @param [event: KeyboardEvent]键盘事件
+ * @param [keyFilter: any] 当前键
+ * @returns Boolean
+ */
+function genFilterKey(event: any, keyFilter: any) {
+  // 浏览器自动补全 input 的时候,会触发 keyDown、keyUp 事件,但此时 event.key 等为空
+  if (!event.key) {
+    return false;
+  }
+
+  // 数字类型直接匹配事件的 keyCode
+  if (isNumber(keyFilter)) {
+    return event.keyCode === keyFilter;
+  }
+  // 字符串依次判断是否有组合键
+  const genArr = keyFilter.split('.');
+  let genLen = 0;
+  for (const key of genArr) {
+    // 组合键
+    const genModifier = modifierKey[key];
+    // key 别名
+    const aliasKey = aliasKeyMap[key];
+    // keyCode 别名
+    const aliasKeyCode = aliasKeyCodeMap[key];
+    /**
+     * 满足以上规则
+     * 1. 自定义组合键别名
+     * 2. 自定义 key 别名
+     * 3. 自定义 keyCode 别名
+     * 4. 匹配 key 或 keyCode
+     */
+    if (
+      (genModifier && genModifier(event)) ||
+      (aliasKey && isArray(aliasKey) ? aliasKey.includes(event.key) : aliasKey === event.key) ||
+      (aliasKeyCode && isArray(aliasKeyCode)
+        ? aliasKeyCode.includes(event.keyCode)
+        : aliasKeyCode === event.keyCode) ||
+      event.key.toUpperCase() === key.toUpperCase()
+    ) {
+      genLen++;
+    }
+  }
+  return genLen === genArr.length;
+}
+
+/**
+ * 键盘输入预处理方法
+ */
+function genKeyFormat(keyFilter: any): KeyPredicate {
+  if (isFunction(keyFilter)) {
+    return keyFilter;
+  }
+  if (isString(keyFilter) || isNumber(keyFilter)) {
+    return (event: KeyboardEvent) => genFilterKey(event, keyFilter);
+  }
+  if (isArray(keyFilter)) {
+    return (event: KeyboardEvent) => keyFilter.some((item: any) => genFilterKey(event, item));
+  }
+  return keyFilter ? () => true : () => false;
+}
+
+export function useKeyPress(
+  keyFilter: KeyFilter,
+  eventHandler: EventHandler = noop,
+  option: EventOption = {}
+) {
+  const { events = defaultEvents, target } = option;
+
+  let el: TargetElement | null | undefined;
+
+  function handler(event: any) {
+    const genGuard: KeyPredicate = genKeyFormat(keyFilter);
+    if (genGuard(event)) {
+      return eventHandler(event);
+    }
+  }
+
+  onMounted(() => {
+    el = getTargetElement(target, window);
+    if (!el) return;
+
+    for (const eventName of events) {
+      el.addEventListener(eventName, handler);
+    }
+  });
+
+  onBeforeUnmount(() => {
+    if (!el) return;
+    for (const eventName of events) {
+      el.removeEventListener(eventName, handler);
+    }
+  });
+}
+
+export function getTargetElement(
+  target?: Target,
+  defaultElement?: TargetElement
+): TargetElement | undefined | null {
+  if (!target) {
+    return defaultElement;
+  }
+
+  let targetElement: TargetElement | undefined | null;
+
+  if (isFunction(target)) {
+    targetElement = target();
+  } else {
+    targetElement = unref(target);
+  }
+  return targetElement;
+}

+ 1 - 1
src/hooks/web/useScript.ts

@@ -16,7 +16,7 @@ export function useScript(opts: ScriptOptions) {
         isLoading.value = false;
         success.value = true;
         error.value = false;
-        resolve();
+        resolve('');
       };
 
       script.onerror = function (err) {