mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
revert to primitive navigation
This commit is contained in:
parent
9ee0b8ffcb
commit
247d6e2a2e
2 changed files with 81 additions and 135 deletions
|
@ -603,7 +603,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
private elementsPendingErasure: ElementsPendingErasure = new Set();
|
private elementsPendingErasure: ElementsPendingErasure = new Set();
|
||||||
|
|
||||||
public flowChartCreator: FlowChartCreator = new FlowChartCreator();
|
public flowChartCreator: FlowChartCreator = new FlowChartCreator();
|
||||||
private flowChartNavigator: FlowChartNavigator = new FlowChartNavigator();
|
private flowChartNavigator: FlowChartNavigator = new FlowChartNavigator(this);
|
||||||
|
|
||||||
hitLinkElement?: NonDeletedExcalidrawElement;
|
hitLinkElement?: NonDeletedExcalidrawElement;
|
||||||
lastPointerDownEvent: React.PointerEvent<HTMLElement> | null = null;
|
lastPointerDownEvent: React.PointerEvent<HTMLElement> | null = null;
|
||||||
|
@ -4143,51 +4143,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
if (selectedElements.length === 1 && arrowKeyPressed) {
|
if (selectedElements.length === 1 && arrowKeyPressed) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
const nextId = this.flowChartNavigator.exploreByDirection(
|
return this.flowChartNavigator.exploreByDirection(
|
||||||
selectedElements[0],
|
selectedElements[0],
|
||||||
this.scene.getNonDeletedElementsMap(),
|
|
||||||
getLinkDirectionFromKey(event.key),
|
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,9 @@ import { invariant, toBrandedType } from "../utils";
|
||||||
import { pointFrom, type LocalPoint } from "../../math";
|
import { pointFrom, type LocalPoint } from "../../math";
|
||||||
import { aabbForElement } from "../shapes";
|
import { aabbForElement } from "../shapes";
|
||||||
import { updateElbowArrowPoints } from "./elbowArrow";
|
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";
|
type LinkDirection = "up" | "right" | "down" | "left";
|
||||||
|
|
||||||
|
@ -491,62 +494,64 @@ const createBindingArrow = (
|
||||||
|
|
||||||
export class FlowChartNavigator {
|
export class FlowChartNavigator {
|
||||||
isExploring: boolean = false;
|
isExploring: boolean = false;
|
||||||
// nodes that are ONE link away (successor and predecessor both included)
|
|
||||||
private sameLevelNodes: ExcalidrawElement[] = [];
|
private app: App;
|
||||||
private sameLevelIndex: number = 0;
|
private siblingNodes: ExcalidrawElement[] = [];
|
||||||
// set it to the opposite of the defalut creation direction
|
private siblingIndex: number = 0;
|
||||||
private direction: LinkDirection | null = null;
|
private direction: LinkDirection | null = null;
|
||||||
// for speedier navigation
|
|
||||||
private visitedNodes: Set<ExcalidrawElement["id"]> = new Set();
|
constructor(app: App) {
|
||||||
|
this.app = app;
|
||||||
|
}
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
this.isExploring = false;
|
this.isExploring = false;
|
||||||
this.sameLevelNodes = [];
|
this.siblingNodes = [];
|
||||||
this.sameLevelIndex = 0;
|
this.siblingIndex = 0;
|
||||||
this.direction = null;
|
this.direction = null;
|
||||||
this.visitedNodes.clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exploreByDirection(
|
/**
|
||||||
element: ExcalidrawElement,
|
* Explore the flowchart by the given direction.
|
||||||
elementsMap: ElementsMap,
|
*
|
||||||
direction: LinkDirection,
|
* The exploration follows a (near) breadth-first approach: when there're multiple
|
||||||
): ExcalidrawElement["id"] | null {
|
* 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)) {
|
if (!isBindableElement(element)) {
|
||||||
return null;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const elementsMap = this.app.scene.getNonDeletedElementsMap();
|
||||||
|
|
||||||
// clear if going at a different direction
|
// clear if going at a different direction
|
||||||
if (direction !== this.direction) {
|
if (direction !== this.direction) {
|
||||||
this.clear();
|
this.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
// add the current node to the visited
|
|
||||||
if (!this.visitedNodes.has(element.id)) {
|
|
||||||
this.visitedNodes.add(element.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CASE:
|
* if we're already exploring (holding the alt key)
|
||||||
* - already started exploring, AND
|
* and the direction is the same as the previous one
|
||||||
* - there are multiple nodes at the same level, AND
|
* and there're multiple nodes at the same level
|
||||||
* - still going at the same direction, AND
|
* then we should traverse through them before moving to the next level
|
||||||
*
|
|
||||||
* RESULT:
|
|
||||||
* - loop through nodes at the same level
|
|
||||||
*
|
|
||||||
* WHY:
|
|
||||||
* - provides user the capability to loop through nodes at the same level
|
|
||||||
*/
|
*/
|
||||||
if (
|
if (
|
||||||
this.isExploring &&
|
this.isExploring &&
|
||||||
direction === this.direction &&
|
direction === this.direction &&
|
||||||
this.sameLevelNodes.length > 1
|
this.siblingNodes.length > 1
|
||||||
) {
|
) {
|
||||||
this.sameLevelIndex =
|
this.siblingIndex++;
|
||||||
(this.sameLevelIndex + 1) % this.sameLevelNodes.length;
|
|
||||||
|
|
||||||
return this.sameLevelNodes[this.sameLevelIndex].id;
|
// there're more unexplored nodes at the same level
|
||||||
|
if (this.siblingIndex < this.siblingNodes.length) {
|
||||||
|
return this.goToNode(this.siblingNodes[this.siblingIndex].id);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.goToNode(this.siblingNodes[0].id);
|
||||||
|
this.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
const nodes = [
|
const nodes = [
|
||||||
|
@ -554,70 +559,52 @@ export class FlowChartNavigator {
|
||||||
...getPredecessors(element, elementsMap, direction),
|
...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) {
|
if (nodes.length > 0) {
|
||||||
this.sameLevelIndex = 0;
|
this.siblingIndex = 0;
|
||||||
this.isExploring = true;
|
this.isExploring = true;
|
||||||
this.sameLevelNodes = nodes;
|
this.siblingNodes = nodes;
|
||||||
this.direction = direction;
|
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private goToNode = (nodeId: ExcalidrawElement["id"]) => {
|
||||||
|
this.app.setState((prevState) => ({
|
||||||
|
selectedElementIds: makeNextSelectedElementIds(
|
||||||
|
{
|
||||||
|
[nodeId]: true,
|
||||||
|
},
|
||||||
|
prevState,
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const nextNode = this.app.scene.getNonDeletedElementsMap().get(nodeId);
|
||||||
|
|
||||||
|
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 {
|
export class FlowChartCreator {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue