Yuki Takei 5 лет назад
Родитель
Сommit
686ab4487b

+ 34 - 22
src/client/js/components/Hotkeys/HotkeysDetector.jsx

@@ -1,13 +1,26 @@
-import React, { useCallback } from 'react';
+import React, { useMemo, useCallback } from 'react';
 import PropTypes from 'prop-types';
 
 import { GlobalHotKeys } from 'react-hotkeys';
 
-let userCommand = [];
-let processingCommands = [];
+import HotkeyStroke from '../../models/HotkeyStroke';
 
 const HotkeysDetector = (props) => {
 
+  const { keySet, strokeSet, onDetected } = props;
+
+  // memorize HotkeyStroke instances
+  const hotkeyStrokes = useMemo(
+    () => {
+      const strokes = Array.from(strokeSet);
+      return strokes.map(stroke => new HotkeyStroke(stroke));
+    },
+    [strokeSet],
+  );
+
+  /**
+   * return key expression string includes modifier
+   */
   const getKeyExpression = useCallback((event) => {
     let eventKey = event.key;
 
@@ -27,32 +40,30 @@ const HotkeysDetector = (props) => {
     return eventKey;
   }, []);
 
+  /**
+   * evaluate the key user pressed and trigger onDetected
+   */
   const checkHandler = useCallback((event) => {
     event.preventDefault();
 
     const eventKey = getKeyExpression(event);
-    processingCommands = props.hotkeyList;
 
-    userCommand = userCommand.concat(eventKey);
-
-    // filters the corresponding hotkeys(keys) that the user has pressed so far
-    processingCommands = processingCommands.filter((value) => {
-      return value.slice(0, userCommand.length).toString() === userCommand.toString();
+    hotkeyStrokes.forEach((hotkeyStroke) => {
+      if (hotkeyStroke.evaluate(eventKey)) {
+        onDetected(hotkeyStroke.stroke);
+      }
     });
+  }, [hotkeyStrokes, getKeyExpression, onDetected]);
 
-    // executes if there were keymap that matches what the user pressed fully.
-    if ((processingCommands.length === 1) && (props.hotkeyList.find(ary => ary.toString() === userCommand.toString()))) {
-      props.onDetected(processingCommands[0]);
-      userCommand = [];
-    }
-    else if (processingCommands.toString() === [].toString()) {
-      userCommand = [];
-    }
-  }, [props, getKeyExpression]);
+  // memorize keyMap for GlobalHotKeys
+  const keyMap = useMemo(() => {
+    return { check: Array.from(keySet) };
+  }, [keySet]);
 
-  const keySet = new Set(props.hotkeyList);
-  const keyMap = { check: Array.from(keySet) };
-  const handlers = { check: checkHandler };
+  // memorize handlers for GlobalHotKeys
+  const handlers = useMemo(() => {
+    return { check: checkHandler };
+  }, [checkHandler]);
 
   return (
     <GlobalHotKeys keyMap={keyMap} handlers={handlers} />
@@ -62,7 +73,8 @@ const HotkeysDetector = (props) => {
 
 HotkeysDetector.propTypes = {
   onDetected: PropTypes.func.isRequired,
-  hotkeyList: PropTypes.array.isRequired,
+  keySet: PropTypes.instanceOf(Set).isRequired,
+  strokeSet: PropTypes.instanceOf(Set).isRequired,
 };
 
 export default HotkeysDetector;

+ 22 - 6
src/client/js/components/Hotkeys/HotkeysManager.jsx

@@ -16,11 +16,22 @@ const SUPPORTED_COMPONENTS = [
   EditPage,
 ];
 
+const KEY_SET = new Set();
+const STROKE_SET = new Set();
 const STROKE_TO_COMPONENT_MAP = {};
+
 SUPPORTED_COMPONENTS.forEach((comp) => {
   const strokes = comp.getHotkeyStrokes();
+
   strokes.forEach((stroke) => {
-    STROKE_TO_COMPONENT_MAP[stroke] = comp;
+    // register key
+    stroke.forEach(key => KEY_SET.add(key));
+    // register stroke
+    STROKE_SET.add(stroke);
+    // register component
+    const componentList = STROKE_TO_COMPONENT_MAP[stroke] || [];
+    componentList.push(comp);
+    STROKE_TO_COMPONENT_MAP[stroke.toString()] = componentList;
   });
 });
 
@@ -43,16 +54,21 @@ const HotkeysManager = (props) => {
    */
   const onDetected = (strokeDetermined) => {
     const key = (Math.random() * 1000).toString();
-    const Component = STROKE_TO_COMPONENT_MAP[strokeDetermined.toString()];
-    const newComponent = <Component key={key} onDeleteRender={deleteRender} />;
+    const components = STROKE_TO_COMPONENT_MAP[strokeDetermined.toString()];
 
-    const newView = view.concat(newComponent).flat();
-    setView(newView);
+    const newViews = components.map(Component => (
+      <Component key={key} onDeleteRender={deleteRender} />
+    ));
+    setView(view.concat(newViews).flat());
   };
 
   return (
     <>
-      <HotkeysDetector onDetected={stroke => onDetected(stroke)} hotkeyList={Object.keys(STROKE_TO_COMPONENT_MAP)} />
+      <HotkeysDetector
+        onDetected={stroke => onDetected(stroke)}
+        keySet={KEY_SET}
+        strokeSet={STROKE_SET}
+      />
       {view}
     </>
   );

+ 57 - 0
src/client/js/models/HotkeyStroke.js

@@ -0,0 +1,57 @@
+import loggerFactory from '@alias/logger';
+
+const logger = loggerFactory('growi:cli:HotkeyStroke');
+
+export default class HotkeyStroke {
+
+  constructor(stroke) {
+    this.stroke = stroke;
+    this.activeIndices = [];
+  }
+
+  get firstKey() {
+    return this.stroke[0];
+  }
+
+  /**
+   * Evaluate whether the specified key completes stroke or not
+   * @param {string} key
+   * @return T/F whether the specified key completes stroke or not
+   */
+  evaluate(key) {
+    if (key === this.firstKey) {
+      // add a new active index
+      this.activeIndices.push(0);
+    }
+
+    let isCompleted = false;
+    this.activeIndices = this.activeIndices
+      .map((index) => {
+        // return null when key does not match
+        if (key !== this.stroke[index]) {
+          return null;
+        }
+
+        const nextIndex = index + 1;
+
+        if (this.stroke.length <= nextIndex) {
+          isCompleted = true;
+          return null;
+        }
+
+        return nextIndex;
+      })
+      // exclude null
+      .filter(index => index != null);
+
+    // reset if completed
+    if (isCompleted) {
+      this.activeIndices = [];
+    }
+
+    logger.debug('activeIndices for [', this.stroke, '] => [', this.activeIndices, ']');
+
+    return isCompleted;
+  }
+
+}