Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 120 additions & 1 deletion src/keyboard_drag_strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,143 @@
* SPDX-License-Identifier: Apache-2.0
*/

import {dragging, utils} from 'blockly';
import {ASTNode, BlockSvg, RenderedConnection, dragging, utils} from 'blockly';
import {Direction, getDirectionFromXY} from './drag_direction';
import {LineCursor} from './line_cursor';

// Copied in from core because it is not exported.
interface ConnectionCandidate {
/** A connection on the dragging stack that is compatible with neighbour. */
local: RenderedConnection;

/** A nearby connection that is compatible with local. */
neighbour: RenderedConnection;

/** The distance between the local connection and the neighbour connection. */
distance: number;
}

// @ts-expect-error overrides a private function.
export class KeyboardDragStrategy extends dragging.BlockDragStrategy {
/** Which direction the current constrained drag is in, if any. */
private currentDragDirection: Direction | null = null;

/** Where a constrained movement should start when traversing the tree. */
private searchNode: ASTNode | null = null;

override startDrag(e?: PointerEvent) {
super.startDrag(e);
// Set position of the dragging block, so that it doesn't pop
// to the top left of the workspace.
// @ts-expect-error block and startLoc are private.
this.block.moveDuringDrag(this.startLoc);
// @ts-expect-error startParentConn is private.
this.searchNode = ASTNode.createConnectionNode(this.startParentConn);
}

override drag(newLoc: utils.Coordinate, e?: PointerEvent): void {
if (!e) return;
this.currentDragDirection = getDirectionFromXY({x: e.tiltX, y: e.tiltY});
super.drag(newLoc);

// Handle the case when an unconstrained drag found a connection candidate.
// The next constrained move will resume the search from the current candidate
// location.
// @ts-expect-error connectionCandidate is private.
if (this.connectionCandidate) {
this.searchNode = ASTNode.createConnectionNode(
// @ts-expect-error connectionCandidate is private.
(this.connectionCandidate as ConnectionCandidate).neighbour,
);
}
}

/**
* Returns the next compatible connection in keyboard navigation order,
* based on the input direction.
* Always resumes the search at the last valid connection that was tried.
*
* @param draggingBlock The block where the drag started.
* @returns A valid connection candidate, or null if none was found.
*/
private getConstrainedConnectionCandidate(
draggingBlock: BlockSvg,
): ConnectionCandidate | null {
// TODO(#385): Make sure this works for any cursor, not just LineCursor.
const cursor = draggingBlock.workspace.getCursor() as LineCursor;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

even when line cursor is the default in core, we'll still let people have other types right? can you leave a comment or something to remind us that we'll need to adjust this logic as well

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a todo and filed an issue.


const initialNode = this.searchNode;
if (!initialNode || !cursor) return null;

// @ts-expect-error getLocalConnections is private.
const localConns = this.getLocalConnections(draggingBlock);
const connectionChecker = draggingBlock.workspace.connectionChecker;

let candidateConnection: ConnectionCandidate | null = null;

let potential: ASTNode | null = initialNode;
while (potential && !candidateConnection) {
if (
this.currentDragDirection === Direction.Up ||
this.currentDragDirection === Direction.Left
) {
potential = cursor.getPreviousNode(potential, (node) => {
// @ts-expect-error isConnectionType is private.
return node && ASTNode.isConnectionType(node.getType());
});
} else if (
this.currentDragDirection === Direction.Down ||
this.currentDragDirection === Direction.Right
) {
potential = cursor.getNextNode(potential, (node) => {
// @ts-expect-error isConnectionType is private.
return node && ASTNode.isConnectionType(node.getType());
});
}

localConns.forEach((conn: RenderedConnection) => {
const potentialLocation =
potential?.getLocation() as RenderedConnection;
if (
connectionChecker.canConnect(conn, potentialLocation, true, Infinity)
) {
candidateConnection = {
local: conn,
neighbour: potentialLocation,
distance: 0,
};
}
});
}
if (candidateConnection) {
this.searchNode = ASTNode.createConnectionNode(
(candidateConnection as ConnectionCandidate).neighbour,
);
}
return candidateConnection;
}

override currCandidateIsBetter(
currCandidate: ConnectionCandidate,
delta: utils.Coordinate,
newCandidate: ConnectionCandidate,
): boolean {
if (this.isConstrainedMovement()) {
return false; // New connection is always better during a constrained drag.
}
// @ts-expect-error currCandidateIsBetter is private.
return super.currCandidateIsBetter(currCandidate, delta, newCandidate);
}

override getConnectionCandidate(
draggingBlock: BlockSvg,
delta: utils.Coordinate,
): ConnectionCandidate | null {
if (this.isConstrainedMovement()) {
return this.getConstrainedConnectionCandidate(draggingBlock);
}
// @ts-expect-error getConnctionCandidate is private.
return super.getConnectionCandidate(draggingBlock, delta);
}

/**
Expand Down
4 changes: 2 additions & 2 deletions src/line_cursor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ export class LineCursor extends Marker {
* should be traversed.
* @returns The next node in the traversal.
*/
private getNextNode(
getNextNode(
node: ASTNode | null,
isValid: (p1: ASTNode | null) => boolean,
): ASTNode | null {
Expand Down Expand Up @@ -347,7 +347,7 @@ export class LineCursor extends Marker {
* @returns The previous node in the traversal or null if no previous node
* exists.
*/
private getPreviousNode(
getPreviousNode(
node: ASTNode | null,
isValid: (p1: ASTNode | null) => boolean,
): ASTNode | null {
Expand Down