Skip to content

Commit c38e11d

Browse files
feat: add constrained movement (#384)
* feat: add constrained movement * chore: cleanup
1 parent 23231d3 commit c38e11d

File tree

2 files changed

+122
-3
lines changed

2 files changed

+122
-3
lines changed

src/keyboard_drag_strategy.ts

Lines changed: 120 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,143 @@
44
* SPDX-License-Identifier: Apache-2.0
55
*/
66

7-
import {dragging, utils} from 'blockly';
7+
import {ASTNode, BlockSvg, RenderedConnection, dragging, utils} from 'blockly';
88
import {Direction, getDirectionFromXY} from './drag_direction';
9+
import {LineCursor} from './line_cursor';
910

11+
// Copied in from core because it is not exported.
12+
interface ConnectionCandidate {
13+
/** A connection on the dragging stack that is compatible with neighbour. */
14+
local: RenderedConnection;
15+
16+
/** A nearby connection that is compatible with local. */
17+
neighbour: RenderedConnection;
18+
19+
/** The distance between the local connection and the neighbour connection. */
20+
distance: number;
21+
}
22+
23+
// @ts-expect-error overrides a private function.
1024
export class KeyboardDragStrategy extends dragging.BlockDragStrategy {
25+
/** Which direction the current constrained drag is in, if any. */
1126
private currentDragDirection: Direction | null = null;
1227

28+
/** Where a constrained movement should start when traversing the tree. */
29+
private searchNode: ASTNode | null = null;
30+
1331
override startDrag(e?: PointerEvent) {
1432
super.startDrag(e);
1533
// Set position of the dragging block, so that it doesn't pop
1634
// to the top left of the workspace.
1735
// @ts-expect-error block and startLoc are private.
1836
this.block.moveDuringDrag(this.startLoc);
37+
// @ts-expect-error startParentConn is private.
38+
this.searchNode = ASTNode.createConnectionNode(this.startParentConn);
1939
}
2040

2141
override drag(newLoc: utils.Coordinate, e?: PointerEvent): void {
2242
if (!e) return;
2343
this.currentDragDirection = getDirectionFromXY({x: e.tiltX, y: e.tiltY});
2444
super.drag(newLoc);
45+
46+
// Handle the case when an unconstrained drag found a connection candidate.
47+
// The next constrained move will resume the search from the current candidate
48+
// location.
49+
// @ts-expect-error connectionCandidate is private.
50+
if (this.connectionCandidate) {
51+
this.searchNode = ASTNode.createConnectionNode(
52+
// @ts-expect-error connectionCandidate is private.
53+
(this.connectionCandidate as ConnectionCandidate).neighbour,
54+
);
55+
}
56+
}
57+
58+
/**
59+
* Returns the next compatible connection in keyboard navigation order,
60+
* based on the input direction.
61+
* Always resumes the search at the last valid connection that was tried.
62+
*
63+
* @param draggingBlock The block where the drag started.
64+
* @returns A valid connection candidate, or null if none was found.
65+
*/
66+
private getConstrainedConnectionCandidate(
67+
draggingBlock: BlockSvg,
68+
): ConnectionCandidate | null {
69+
// TODO(#385): Make sure this works for any cursor, not just LineCursor.
70+
const cursor = draggingBlock.workspace.getCursor() as LineCursor;
71+
72+
const initialNode = this.searchNode;
73+
if (!initialNode || !cursor) return null;
74+
75+
// @ts-expect-error getLocalConnections is private.
76+
const localConns = this.getLocalConnections(draggingBlock);
77+
const connectionChecker = draggingBlock.workspace.connectionChecker;
78+
79+
let candidateConnection: ConnectionCandidate | null = null;
80+
81+
let potential: ASTNode | null = initialNode;
82+
while (potential && !candidateConnection) {
83+
if (
84+
this.currentDragDirection === Direction.Up ||
85+
this.currentDragDirection === Direction.Left
86+
) {
87+
potential = cursor.getPreviousNode(potential, (node) => {
88+
// @ts-expect-error isConnectionType is private.
89+
return node && ASTNode.isConnectionType(node.getType());
90+
});
91+
} else if (
92+
this.currentDragDirection === Direction.Down ||
93+
this.currentDragDirection === Direction.Right
94+
) {
95+
potential = cursor.getNextNode(potential, (node) => {
96+
// @ts-expect-error isConnectionType is private.
97+
return node && ASTNode.isConnectionType(node.getType());
98+
});
99+
}
100+
101+
localConns.forEach((conn: RenderedConnection) => {
102+
const potentialLocation =
103+
potential?.getLocation() as RenderedConnection;
104+
if (
105+
connectionChecker.canConnect(conn, potentialLocation, true, Infinity)
106+
) {
107+
candidateConnection = {
108+
local: conn,
109+
neighbour: potentialLocation,
110+
distance: 0,
111+
};
112+
}
113+
});
114+
}
115+
if (candidateConnection) {
116+
this.searchNode = ASTNode.createConnectionNode(
117+
(candidateConnection as ConnectionCandidate).neighbour,
118+
);
119+
}
120+
return candidateConnection;
121+
}
122+
123+
override currCandidateIsBetter(
124+
currCandidate: ConnectionCandidate,
125+
delta: utils.Coordinate,
126+
newCandidate: ConnectionCandidate,
127+
): boolean {
128+
if (this.isConstrainedMovement()) {
129+
return false; // New connection is always better during a constrained drag.
130+
}
131+
// @ts-expect-error currCandidateIsBetter is private.
132+
return super.currCandidateIsBetter(currCandidate, delta, newCandidate);
133+
}
134+
135+
override getConnectionCandidate(
136+
draggingBlock: BlockSvg,
137+
delta: utils.Coordinate,
138+
): ConnectionCandidate | null {
139+
if (this.isConstrainedMovement()) {
140+
return this.getConstrainedConnectionCandidate(draggingBlock);
141+
}
142+
// @ts-expect-error getConnctionCandidate is private.
143+
return super.getConnectionCandidate(draggingBlock, delta);
25144
}
26145

27146
/**

src/line_cursor.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,7 @@ export class LineCursor extends Marker {
314314
* should be traversed.
315315
* @returns The next node in the traversal.
316316
*/
317-
private getNextNode(
317+
getNextNode(
318318
node: ASTNode | null,
319319
isValid: (p1: ASTNode | null) => boolean,
320320
): ASTNode | null {
@@ -347,7 +347,7 @@ export class LineCursor extends Marker {
347347
* @returns The previous node in the traversal or null if no previous node
348348
* exists.
349349
*/
350-
private getPreviousNode(
350+
getPreviousNode(
351351
node: ASTNode | null,
352352
isValid: (p1: ASTNode | null) => boolean,
353353
): ASTNode | null {

0 commit comments

Comments
 (0)