snapshot of https://github.com/excalidraw/excalidraw.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Tests / test (push) Failing after 15s
Some checks failed
Tests / test (push) Failing after 15s
This commit is contained in:
198
packages/excalidraw/components/App.flowchart.ts
Normal file
198
packages/excalidraw/components/App.flowchart.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
import { isArrowKey, KEYS } from "@excalidraw/common";
|
||||
|
||||
import {
|
||||
makeNextSelectedElementIds,
|
||||
CaptureUpdateAction,
|
||||
FlowChartCreator,
|
||||
FlowChartNavigator,
|
||||
getSelectedElements,
|
||||
isFlowchartNodeElement,
|
||||
type LinkDirection,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import type {
|
||||
ExcalidrawElement,
|
||||
NonDeletedExcalidrawElement,
|
||||
} from "@excalidraw/element/types";
|
||||
|
||||
import type React from "react";
|
||||
import type App from "./App";
|
||||
import type { PendingExcalidrawElements } from "../types";
|
||||
|
||||
type FlowchartOperation =
|
||||
| { type: "none" }
|
||||
| { type: "canceled" }
|
||||
| { type: "creating"; pending: PendingExcalidrawElements }
|
||||
| { type: "navigating"; nodeId: ExcalidrawElement["id"] | null }
|
||||
| { type: "committed"; nodes: PendingExcalidrawElements }
|
||||
| { type: "navigationEnded" };
|
||||
|
||||
/**
|
||||
* Captures the App state management for the flowchart functionality.
|
||||
*/
|
||||
export class AppFlowchart {
|
||||
private creator = new FlowChartCreator();
|
||||
private navigator = new FlowChartNavigator();
|
||||
|
||||
constructor(private app: App) {}
|
||||
|
||||
get pendingNodes() {
|
||||
return this.creator.pendingNodes;
|
||||
}
|
||||
|
||||
get isCreatingChart() {
|
||||
return this.creator.isCreatingChart;
|
||||
}
|
||||
|
||||
/** ends any in-progress flowchart creation/navigation session */
|
||||
clear = () => {
|
||||
this.creator.clear();
|
||||
this.navigator.clear();
|
||||
};
|
||||
|
||||
handleKeyEvent = (event: React.KeyboardEvent | KeyboardEvent): boolean => {
|
||||
const operation = this.resolveKeyboardEventToOperation(event);
|
||||
|
||||
switch (operation.type) {
|
||||
case "none":
|
||||
return false;
|
||||
case "canceled":
|
||||
this.app.triggerRender(true);
|
||||
return true;
|
||||
case "creating":
|
||||
event.preventDefault();
|
||||
if (operation.pending.length) {
|
||||
this.app.revealIfHidden(operation.pending);
|
||||
}
|
||||
return true;
|
||||
case "navigating": {
|
||||
event.preventDefault();
|
||||
const node =
|
||||
operation.nodeId &&
|
||||
this.app.scene.getNonDeletedElementsMap().get(operation.nodeId);
|
||||
if (node) {
|
||||
this.selectAndReveal(node);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case "committed": {
|
||||
if (operation.nodes.length) {
|
||||
this.app.insertNewElements(operation.nodes);
|
||||
}
|
||||
|
||||
const firstNode = operation.nodes[0];
|
||||
if (firstNode) {
|
||||
this.selectAndReveal(firstNode);
|
||||
}
|
||||
|
||||
this.captureUpdate();
|
||||
return true;
|
||||
}
|
||||
case "navigationEnded":
|
||||
this.captureUpdate();
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
private resolveKeyboardEventToOperation(
|
||||
event: React.KeyboardEvent | KeyboardEvent,
|
||||
): FlowchartOperation {
|
||||
const { creator, navigator, app } = this;
|
||||
|
||||
if (event.type === "keydown") {
|
||||
if (event.key === KEYS.ESCAPE && creator.isCreatingChart) {
|
||||
creator.clear();
|
||||
return { type: "canceled" };
|
||||
}
|
||||
|
||||
if (!isArrowKey(event.key)) {
|
||||
return { type: "none" };
|
||||
}
|
||||
|
||||
if (event[KEYS.CTRL_OR_CMD] && !event.shiftKey) {
|
||||
const selectedElements = getSelectedElements(
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
app.state,
|
||||
);
|
||||
|
||||
if (
|
||||
selectedElements.length === 1 &&
|
||||
isFlowchartNodeElement(selectedElements[0])
|
||||
) {
|
||||
creator.createNodes(
|
||||
selectedElements[0],
|
||||
app.state,
|
||||
AppFlowchart.getLinkDirectionFromKey(event.key),
|
||||
app.scene,
|
||||
);
|
||||
}
|
||||
|
||||
return { type: "creating", pending: creator.pendingNodes ?? [] };
|
||||
}
|
||||
|
||||
if (event.altKey) {
|
||||
const elementsMap = app.scene.getNonDeletedElementsMap();
|
||||
const selectedElements = getSelectedElements(elementsMap, app.state);
|
||||
|
||||
if (selectedElements.length === 1) {
|
||||
return {
|
||||
type: "navigating",
|
||||
nodeId: navigator.exploreByDirection(
|
||||
selectedElements[0],
|
||||
elementsMap,
|
||||
AppFlowchart.getLinkDirectionFromKey(event.key),
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { type: "none" };
|
||||
}
|
||||
|
||||
// keyup: releasing a modifier finalizes the workflow it was driving;
|
||||
// both can finalize on the same event
|
||||
const navigationEnded = !event.altKey && navigator.isExploring;
|
||||
if (navigationEnded) {
|
||||
navigator.clear();
|
||||
}
|
||||
|
||||
if (!event[KEYS.CTRL_OR_CMD] && creator.isCreatingChart) {
|
||||
const nodes = creator.pendingNodes ?? [];
|
||||
creator.clear();
|
||||
return { type: "committed", nodes };
|
||||
}
|
||||
|
||||
return navigationEnded ? { type: "navigationEnded" } : { type: "none" };
|
||||
}
|
||||
|
||||
private selectAndReveal(node: NonDeletedExcalidrawElement) {
|
||||
this.app.setState((prevState) => ({
|
||||
selectedElementIds: makeNextSelectedElementIds(
|
||||
{ [node.id]: true },
|
||||
prevState,
|
||||
),
|
||||
}));
|
||||
this.app.revealIfHidden([node]);
|
||||
}
|
||||
|
||||
private captureUpdate() {
|
||||
this.app.syncActionResult({
|
||||
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
||||
});
|
||||
}
|
||||
|
||||
private static getLinkDirectionFromKey(key: string): LinkDirection {
|
||||
switch (key) {
|
||||
case KEYS.ARROW_UP:
|
||||
return "up";
|
||||
case KEYS.ARROW_DOWN:
|
||||
return "down";
|
||||
case KEYS.ARROW_RIGHT:
|
||||
return "right";
|
||||
case KEYS.ARROW_LEFT:
|
||||
return "left";
|
||||
default:
|
||||
return "right";
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user