diff --git a/packages/blockly/core/dragging/block_drag_strategy.ts b/packages/blockly/core/dragging/block_drag_strategy.ts index a84f4a3cccf..276c55a5a80 100644 --- a/packages/blockly/core/dragging/block_drag_strategy.ts +++ b/packages/blockly/core/dragging/block_drag_strategy.ts @@ -15,6 +15,7 @@ import {ConnectionType} from '../connection_type.js'; import type {BlockMove} from '../events/events_block_move.js'; import {EventType} from '../events/type.js'; import * as eventUtils from '../events/utils.js'; +import {FocusManager} from '../focus_manager.js'; import {showUnconstrainedMoveHint} from '../hints.js'; import type {IBubble} from '../interfaces/i_bubble.js'; import type {IConnectionPreviewer} from '../interfaces/i_connection_previewer.js'; @@ -243,10 +244,6 @@ export class BlockDragStrategy implements IDragStrategy { } this.block.setDragging(true); - this.workspace.getLayerManager()?.moveToDragLayer(this.block); - this.getVisibleBubbles(this.block).forEach((bubble) => { - this.workspace.getLayerManager()?.moveToDragLayer(bubble, false); - }); // For keyboard-driven moves, cache a list of valid connection points for // use in constrained moved mode. @@ -254,7 +251,6 @@ export class BlockDragStrategy implements IDragStrategy { this.cacheAllConnectionPairs(); // Scooch the block to be offset from the connection preview indicator. - this.block.moveDuringDrag(this.startLoc); const neighbour = this.updateConnectionPreview( this.block, new Coordinate(0, 0), @@ -262,10 +258,11 @@ export class BlockDragStrategy implements IDragStrategy { if (neighbour) { let offset: Coordinate; if (neighbour.type === ConnectionType.PREVIOUS_STATEMENT) { - const origin = this.block.getRelativeToSurfaceXY(); offset = new Coordinate( - origin.x + this.BLOCK_CONNECTION_OFFSET, - origin.y - this.BLOCK_CONNECTION_OFFSET, + neighbour.x, + neighbour.y - + this.block.getHeightWidth().height - + this.BLOCK_CONNECTION_OFFSET, ); } else { offset = new Coordinate( @@ -275,8 +272,15 @@ export class BlockDragStrategy implements IDragStrategy { } this.block.moveDuringDrag(offset); } + } else { + this.block.moveDuringDrag(this.startLoc); } + this.workspace.getLayerManager()?.moveToDragLayer(this.block); + this.getVisibleBubbles(this.block).forEach((bubble) => { + this.workspace.getLayerManager()?.moveToDragLayer(bubble, false); + }); + this.announceMove(true); return this.block; } @@ -388,7 +392,7 @@ export class BlockDragStrategy implements IDragStrategy { * the initial connection pair is also used as the first connection candidate. */ private storeInitialConnections(healStack: boolean) { - // Prioritze the block's parent connection (output or previous) if one exists. + // Prioritize the block's parent connection (output or previous) if one exists. let localParentConn: RenderedConnection | null = null; let parentTargetConn: RenderedConnection | null = null; @@ -536,7 +540,8 @@ export class BlockDragStrategy implements IDragStrategy { delta: Coordinate, ): RenderedConnection | undefined { const currCandidate = this.connectionCandidate; - const newCandidate = this.getConnectionCandidate(delta); + const newCandidate = + this.getInitialCandidate() ?? this.getConnectionCandidate(delta); if (!newCandidate) { // Position above or below the first/last block. @@ -584,6 +589,8 @@ export class BlockDragStrategy implements IDragStrategy { ? currCandidate : newCandidate; this.connectionCandidate = candidate; + console.log('updated'); + console.log(this.connectionCandidate); const {local, neighbour} = candidate; const localIsOutputOrPrevious = @@ -1049,4 +1056,74 @@ export class BlockDragStrategy implements IDragStrategy { return connections as RenderedConnection[]; } + + /** + * Returns a connection candidate to move the dragged block to at the start of + * a drag. If the passively focused node is a connection and the dragged block + * can connect to it, the connection will be returned. Otherwise, the first + * compatible connection on the passively focused node's block, if any, will + * be returned. Returns null if the workspace does not have passive focus. + */ + private getInitialCandidate(): ConnectionCandidate | null { + const passiveElement = this.workspace + .getSvgGroup() + .querySelector(`.${FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME}`); + if ( + !passiveElement || + !passiveElement.id || + passiveElement === this.block.getFocusableElement() + ) { + return null; + } + const passiveNode = this.workspace.lookUpFocusableNode(passiveElement.id); + if (!passiveNode) return null; + + const passiveBlock = this.workspace + .getNavigator() + .getSourceBlockFromNode(passiveNode); + if (!passiveBlock) return null; + + const passiveBlockConnections = passiveBlock.getConnections_(false); + let passiveConnection: RenderedConnection | null = null; + + // If the passively focused node is a connection, return it if it is + // compatible with the dragged block. + if (passiveBlockConnections.includes(passiveNode as any)) { + passiveConnection = passiveNode as RenderedConnection; + const connectionChecker = this.block.workspace.connectionChecker; + const local = this.block.getConnections_(false).find((connection) => { + return connectionChecker.canConnect( + connection, + passiveConnection, + true, + Infinity, + ); + }); + + if (local) { + return {local, neighbour: passiveConnection, distance: 0}; + } + } + + // Fall back to returning the first compatible connection on the passively + // focused block, if any. + const pair = this.allConnectionPairs.find( + (pair) => pair.neighbour.getSourceBlock() === passiveBlock, + ); + if (pair) { + return this.pairToCandidate(pair); + } + + // Fall back further to the parent connection of the passively focused + // block, if any. + const outputTarget = passiveBlock.outputConnection?.targetConnection; + const parentPair = this.allConnectionPairs.find( + (pair) => pair.neighbour === outputTarget, + ); + if (parentPair) { + return this.pairToCandidate(parentPair); + } + + return null; + } } diff --git a/packages/blockly/tests/mocha/keyboard_movement_test.js b/packages/blockly/tests/mocha/keyboard_movement_test.js index 21d1cc55e3d..fa5969aa266 100644 --- a/packages/blockly/tests/mocha/keyboard_movement_test.js +++ b/packages/blockly/tests/mocha/keyboard_movement_test.js @@ -21,7 +21,26 @@ import {createKeyDownEvent} from './test_helpers/user_input.js'; suite('Keyboard-driven movement', function () { setup(function () { sharedTestSetup.call(this); - const toolbox = document.getElementById('toolbox-simple'); + const toolbox = { + // There are two kinds of toolboxes. The simpler one is a flyout toolbox. + kind: 'flyoutToolbox', + // The contents is the blocks and other items that exist in your toolbox. + contents: [ + { + kind: 'block', + type: 'text_print', + }, + { + kind: 'block', + type: 'logic_negate', + }, + { + kind: 'block', + type: 'math_number', + }, + ], + }; + this.workspace = Blockly.inject('blocklyDiv', {toolbox: toolbox}); Blockly.common.defineBlocks(p5blocks); Blockly.KeyboardMover.mover.setMoveDistance(20); @@ -554,6 +573,124 @@ suite('Keyboard-driven movement', function () { toastSpy.restore(); }); + test('initially moves the block to the previously-focused statement connection', function () { + const ifBlock = this.workspace.newBlock('controls_if'); + ifBlock.initSvg(); + ifBlock.render(); + + const statementConnection = ifBlock.getInput('DO0').connection; + Blockly.getFocusManager().focusNode(statementConnection); + const t = createKeyDownEvent(Blockly.utils.KeyCodes.T); + this.workspace.getInjectionDiv().dispatchEvent(t); + const enter = createKeyDownEvent(Blockly.utils.KeyCodes.ENTER); + this.workspace.getInjectionDiv().dispatchEvent(enter); + + const movingBlock = Blockly.getFocusManager().getFocusedNode(); + const candidate = movingBlock.getDragStrategy().connectionCandidate; + + assert.equal(candidate.local, movingBlock.previousConnection); + assert.equal(candidate.neighbour, statementConnection); + + const esc = createKeyDownEvent(Blockly.utils.KeyCodes.ESC); + this.workspace.getInjectionDiv().dispatchEvent(esc); + }); + + test("initially moves the block to the previously-focused block's previous connection", function () { + const ifBlock = this.workspace.newBlock('controls_if'); + ifBlock.initSvg(); + ifBlock.render(); + + Blockly.getFocusManager().focusNode(ifBlock); + const t = createKeyDownEvent(Blockly.utils.KeyCodes.T); + this.workspace.getInjectionDiv().dispatchEvent(t); + const enter = createKeyDownEvent(Blockly.utils.KeyCodes.ENTER); + this.workspace.getInjectionDiv().dispatchEvent(enter); + + const movingBlock = Blockly.getFocusManager().getFocusedNode(); + const candidate = movingBlock.getDragStrategy().connectionCandidate; + + assert.equal(candidate.local, movingBlock.nextConnection); + assert.equal(candidate.neighbour, ifBlock.previousConnection); + + const esc = createKeyDownEvent(Blockly.utils.KeyCodes.ESC); + this.workspace.getInjectionDiv().dispatchEvent(esc); + }); + + test('initially moves the block to the previously-focused input connection', function () { + const ifBlock = this.workspace.newBlock('controls_if'); + ifBlock.initSvg(); + ifBlock.render(); + + const inputConnection = ifBlock.getInput('IF0').connection; + Blockly.getFocusManager().focusNode(inputConnection); + const t = createKeyDownEvent(Blockly.utils.KeyCodes.T); + this.workspace.getInjectionDiv().dispatchEvent(t); + const down = createKeyDownEvent(Blockly.utils.KeyCodes.DOWN); + this.workspace.getInjectionDiv().dispatchEvent(down); + const enter = createKeyDownEvent(Blockly.utils.KeyCodes.ENTER); + this.workspace.getInjectionDiv().dispatchEvent(enter); + + const movingBlock = Blockly.getFocusManager().getFocusedNode(); + const candidate = movingBlock.getDragStrategy().connectionCandidate; + + assert.equal(candidate.local, movingBlock.outputConnection); + assert.equal(candidate.neighbour, inputConnection); + + const esc = createKeyDownEvent(Blockly.utils.KeyCodes.ESC); + this.workspace.getInjectionDiv().dispatchEvent(esc); + }); + + test("initially moves the block to the previously-focused block's input connection", function () { + const ifBlock = this.workspace.newBlock('controls_if'); + ifBlock.initSvg(); + ifBlock.render(); + + Blockly.getFocusManager().focusNode(ifBlock); + const t = createKeyDownEvent(Blockly.utils.KeyCodes.T); + this.workspace.getInjectionDiv().dispatchEvent(t); + const down = createKeyDownEvent(Blockly.utils.KeyCodes.DOWN); + this.workspace.getInjectionDiv().dispatchEvent(down); + const enter = createKeyDownEvent(Blockly.utils.KeyCodes.ENTER); + this.workspace.getInjectionDiv().dispatchEvent(enter); + + const movingBlock = Blockly.getFocusManager().getFocusedNode(); + const candidate = movingBlock.getDragStrategy().connectionCandidate; + + assert.equal(candidate.local, movingBlock.outputConnection); + assert.equal(candidate.neighbour, ifBlock.getInput('IF0').connection); + + const esc = createKeyDownEvent(Blockly.utils.KeyCodes.ESC); + this.workspace.getInjectionDiv().dispatchEvent(esc); + }); + + test("initially moves the block to the previously-focused block's parent input connection", function () { + const compare = this.workspace.newBlock('logic_compare'); + compare.initSvg(); + compare.render(); + + const boolean = this.workspace.newBlock('logic_boolean'); + boolean.initSvg(); + boolean.render(); + boolean.outputConnection.connect(compare.getInput('A').connection); + + Blockly.getFocusManager().focusNode(boolean); + const t = createKeyDownEvent(Blockly.utils.KeyCodes.T); + this.workspace.getInjectionDiv().dispatchEvent(t); + const down = createKeyDownEvent(Blockly.utils.KeyCodes.DOWN); + this.workspace.getInjectionDiv().dispatchEvent(down); + const enter = createKeyDownEvent(Blockly.utils.KeyCodes.ENTER); + this.workspace.getInjectionDiv().dispatchEvent(enter); + + const movingBlock = Blockly.getFocusManager().getFocusedNode(); + const candidate = movingBlock.getDragStrategy().connectionCandidate; + + assert.equal(candidate.local, movingBlock.outputConnection); + assert.equal(candidate.neighbour, compare.getInput('A').connection); + + const esc = createKeyDownEvent(Blockly.utils.KeyCodes.ESC); + this.workspace.getInjectionDiv().dispatchEvent(esc); + }); + suite('Statement move tests', function () { // Clear the workspace and load start blocks. setup(function () {