|
4 | 4 | * SPDX-License-Identifier: Apache-2.0 |
5 | 5 | */ |
6 | 6 |
|
7 | | -import {dragging, utils} from 'blockly'; |
| 7 | +import {ASTNode, BlockSvg, RenderedConnection, dragging, utils} from 'blockly'; |
8 | 8 | import {Direction, getDirectionFromXY} from './drag_direction'; |
| 9 | +import {LineCursor} from './line_cursor'; |
9 | 10 |
|
| 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. |
10 | 24 | export class KeyboardDragStrategy extends dragging.BlockDragStrategy { |
| 25 | + /** Which direction the current constrained drag is in, if any. */ |
11 | 26 | private currentDragDirection: Direction | null = null; |
12 | 27 |
|
| 28 | + /** Where a constrained movement should start when traversing the tree. */ |
| 29 | + private searchNode: ASTNode | null = null; |
| 30 | + |
13 | 31 | override startDrag(e?: PointerEvent) { |
14 | 32 | super.startDrag(e); |
15 | 33 | // Set position of the dragging block, so that it doesn't pop |
16 | 34 | // to the top left of the workspace. |
17 | 35 | // @ts-expect-error block and startLoc are private. |
18 | 36 | this.block.moveDuringDrag(this.startLoc); |
| 37 | + // @ts-expect-error startParentConn is private. |
| 38 | + this.searchNode = ASTNode.createConnectionNode(this.startParentConn); |
19 | 39 | } |
20 | 40 |
|
21 | 41 | override drag(newLoc: utils.Coordinate, e?: PointerEvent): void { |
22 | 42 | if (!e) return; |
23 | 43 | this.currentDragDirection = getDirectionFromXY({x: e.tiltX, y: e.tiltY}); |
24 | 44 | 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); |
25 | 144 | } |
26 | 145 |
|
27 | 146 | /** |
|
0 commit comments