Skip to content
Open
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
97 changes: 87 additions & 10 deletions packages/blockly/core/dragging/block_drag_strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -243,29 +244,25 @@ 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.
if (e instanceof KeyboardEvent) {
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),
);
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(
Expand All @@ -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;
}
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -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;
}
}
139 changes: 138 additions & 1 deletion packages/blockly/tests/mocha/keyboard_movement_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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 () {
Expand Down
Loading