Просмотр исходного кода

revert to primitive navigation

Ryan Di 6 месяцев назад
Родитель
Сommit
247d6e2a2e
2 измененных файлов с 79 добавлено и 133 удалено
  1. 2 43
      packages/excalidraw/components/App.tsx
  2. 77 90
      packages/excalidraw/element/flowchart.ts

+ 2 - 43
packages/excalidraw/components/App.tsx

@@ -603,7 +603,7 @@ class App extends React.Component<AppProps, AppState> {
   private elementsPendingErasure: ElementsPendingErasure = new Set();
 
   public flowChartCreator: FlowChartCreator = new FlowChartCreator();
-  private flowChartNavigator: FlowChartNavigator = new FlowChartNavigator();
+  private flowChartNavigator: FlowChartNavigator = new FlowChartNavigator(this);
 
   hitLinkElement?: NonDeletedExcalidrawElement;
   lastPointerDownEvent: React.PointerEvent<HTMLElement> | null = null;
@@ -4143,51 +4143,10 @@ class App extends React.Component<AppProps, AppState> {
           if (selectedElements.length === 1 && arrowKeyPressed) {
             event.preventDefault();
 
-            const nextId = this.flowChartNavigator.exploreByDirection(
+            return this.flowChartNavigator.exploreByDirection(
               selectedElements[0],
-              this.scene.getNonDeletedElementsMap(),
               getLinkDirectionFromKey(event.key),
             );
-
-            if (nextId) {
-              this.setState((prevState) => ({
-                selectedElementIds: makeNextSelectedElementIds(
-                  {
-                    [nextId]: true,
-                  },
-                  prevState,
-                ),
-              }));
-
-              const nextNode = this.scene
-                .getNonDeletedElementsMap()
-                .get(nextId);
-
-              if (
-                nextNode &&
-                !isElementCompletelyInViewport(
-                  [nextNode],
-                  this.canvas.width / window.devicePixelRatio,
-                  this.canvas.height / window.devicePixelRatio,
-                  {
-                    offsetLeft: this.state.offsetLeft,
-                    offsetTop: this.state.offsetTop,
-                    scrollX: this.state.scrollX,
-                    scrollY: this.state.scrollY,
-                    zoom: this.state.zoom,
-                  },
-                  this.scene.getNonDeletedElementsMap(),
-                  this.getEditorUIOffsets(),
-                )
-              ) {
-                this.scrollToContent(nextNode, {
-                  animate: true,
-                  duration: 300,
-                  canvasOffsets: this.getEditorUIOffsets(),
-                });
-              }
-            }
-            return;
           }
         }
       }

+ 77 - 90
packages/excalidraw/element/flowchart.ts

@@ -34,6 +34,9 @@ import { invariant, toBrandedType } from "../utils";
 import { pointFrom, type LocalPoint } from "../../math";
 import { aabbForElement } from "../shapes";
 import { updateElbowArrowPoints } from "./elbowArrow";
+import type App from "../components/App";
+import { makeNextSelectedElementIds } from "../scene/selection";
+import { isElementCompletelyInViewport } from "./sizeHelpers";
 
 type LinkDirection = "up" | "right" | "down" | "left";
 
@@ -491,62 +494,64 @@ const createBindingArrow = (
 
 export class FlowChartNavigator {
   isExploring: boolean = false;
-  // nodes that are ONE link away (successor and predecessor both included)
-  private sameLevelNodes: ExcalidrawElement[] = [];
-  private sameLevelIndex: number = 0;
-  // set it to the opposite of the defalut creation direction
+
+  private app: App;
+  private siblingNodes: ExcalidrawElement[] = [];
+  private siblingIndex: number = 0;
   private direction: LinkDirection | null = null;
-  // for speedier navigation
-  private visitedNodes: Set<ExcalidrawElement["id"]> = new Set();
+
+  constructor(app: App) {
+    this.app = app;
+  }
 
   clear() {
     this.isExploring = false;
-    this.sameLevelNodes = [];
-    this.sameLevelIndex = 0;
+    this.siblingNodes = [];
+    this.siblingIndex = 0;
     this.direction = null;
-    this.visitedNodes.clear();
   }
 
-  exploreByDirection(
-    element: ExcalidrawElement,
-    elementsMap: ElementsMap,
-    direction: LinkDirection,
-  ): ExcalidrawElement["id"] | null {
+  /**
+   * Explore the flowchart by the given direction.
+   *
+   * The exploration follows a (near) breadth-first approach: when there're multiple
+   * nodes at the same level, we allow the user to traverse through them before
+   * moving to the next level.
+   *
+   * Unlike breadth-first search, we return to the first node at the same level.
+   */
+  exploreByDirection(element: ExcalidrawElement, direction: LinkDirection) {
     if (!isBindableElement(element)) {
-      return null;
+      return;
     }
 
+    const elementsMap = this.app.scene.getNonDeletedElementsMap();
+
     // clear if going at a different direction
     if (direction !== this.direction) {
       this.clear();
     }
 
-    // add the current node to the visited
-    if (!this.visitedNodes.has(element.id)) {
-      this.visitedNodes.add(element.id);
-    }
-
     /**
-     * CASE:
-     * - already started exploring, AND
-     * - there are multiple nodes at the same level, AND
-     * - still going at the same direction, AND
-     *
-     * RESULT:
-     * - loop through nodes at the same level
-     *
-     * WHY:
-     * - provides user the capability to loop through nodes at the same level
+     * if we're already exploring (holding the alt key)
+     * and the direction is the same as the previous one
+     * and there're multiple nodes at the same level
+     * then we should traverse through them before moving to the next level
      */
     if (
       this.isExploring &&
       direction === this.direction &&
-      this.sameLevelNodes.length > 1
+      this.siblingNodes.length > 1
     ) {
-      this.sameLevelIndex =
-        (this.sameLevelIndex + 1) % this.sameLevelNodes.length;
+      this.siblingIndex++;
+
+      // there're more unexplored nodes at the same level
+      if (this.siblingIndex < this.siblingNodes.length) {
+        return this.goToNode(this.siblingNodes[this.siblingIndex].id);
+      }
 
-      return this.sameLevelNodes[this.sameLevelIndex].id;
+      this.goToNode(this.siblingNodes[0].id);
+      this.clear();
     }
 
     const nodes = [
@@ -554,70 +559,52 @@ export class FlowChartNavigator {
       ...getPredecessors(element, elementsMap, direction),
     ];
 
-    /**
-     * CASE:
-     * - just started exploring at the given direction
-     *
-     * RESULT:
-     * - go to the first node in the given direction
-     */
     if (nodes.length > 0) {
-      this.sameLevelIndex = 0;
+      this.siblingIndex = 0;
       this.isExploring = true;
-      this.sameLevelNodes = nodes;
+      this.siblingNodes = nodes;
       this.direction = direction;
-      this.visitedNodes.add(nodes[0].id);
 
-      return nodes[0].id;
+      this.goToNode(nodes[0].id);
     }
+  }
 
-    /**
-     * CASE:
-     * - (just started exploring or still going at the same direction) OR
-     * - there're no nodes at the given direction
-     *
-     * RESULT:
-     * - go to some other unvisited linked node
-     *
-     * WHY:
-     * - provide a speedier navigation from a given node to some predecessor
-     *   without the user having to change arrow key
-     */
-    if (direction === this.direction || !this.isExploring) {
-      if (!this.isExploring) {
-        // just started and no other nodes at the given direction
-        // so the current node is technically the first visited node
-        // (this is needed so that we don't get stuck between looping through )
-        this.visitedNodes.add(element.id);
-      }
+  private goToNode = (nodeId: ExcalidrawElement["id"]) => {
+    this.app.setState((prevState) => ({
+      selectedElementIds: makeNextSelectedElementIds(
+        {
+          [nodeId]: true,
+        },
+        prevState,
+      ),
+    }));
 
-      const otherDirections: LinkDirection[] = [
-        "up",
-        "right",
-        "down",
-        "left",
-      ].filter((dir): dir is LinkDirection => dir !== direction);
-
-      const otherLinkedNodes = otherDirections
-        .map((dir) => [
-          ...getSuccessors(element, elementsMap, dir),
-          ...getPredecessors(element, elementsMap, dir),
-        ])
-        .flat()
-        .filter((linkedNode) => !this.visitedNodes.has(linkedNode.id));
-
-      for (const linkedNode of otherLinkedNodes) {
-        if (!this.visitedNodes.has(linkedNode.id)) {
-          this.visitedNodes.add(linkedNode.id);
-          this.isExploring = true;
-          this.direction = direction;
-          return linkedNode.id;
-        }
-      }
-    }
+    const nextNode = this.app.scene.getNonDeletedElementsMap().get(nodeId);
 
-    return null;
-  }
+    if (
+      nextNode &&
+      !isElementCompletelyInViewport(
+        [nextNode],
+        this.app.canvas.width / window.devicePixelRatio,
+        this.app.canvas.height / window.devicePixelRatio,
+        {
+          offsetLeft: this.app.state.offsetLeft,
+          offsetTop: this.app.state.offsetTop,
+          scrollX: this.app.state.scrollX,
+          scrollY: this.app.state.scrollY,
+          zoom: this.app.state.zoom,
+        },
+        this.app.scene.getNonDeletedElementsMap(),
+        this.app.getEditorUIOffsets(),
+      )
+    ) {
+      this.app.scrollToContent(nextNode, {
+        animate: true,
+        duration: 300,
+        canvasOffsets: this.app.getEditorUIOffsets(),
+      });
+    }
+  };
 }
 
 export class FlowChartCreator {