mirror of
https://github.com/google/blockly.git
synced 2025-12-16 06:10:12 +01:00
fix: Fix navigation for blocks with multiple statement inputs. (#9143)
* fix: Fix navigation for blocks with multiple statement inputs. * chore: Add tests to prevent regressions.
This commit is contained in:
@@ -24,7 +24,7 @@ export class BlockNavigationPolicy implements INavigationPolicy<BlockSvg> {
|
|||||||
* @returns The first field or input of the given block, if any.
|
* @returns The first field or input of the given block, if any.
|
||||||
*/
|
*/
|
||||||
getFirstChild(current: BlockSvg): IFocusableNode | null {
|
getFirstChild(current: BlockSvg): IFocusableNode | null {
|
||||||
const candidates = getBlockNavigationCandidates(current);
|
const candidates = getBlockNavigationCandidates(current, true);
|
||||||
return candidates[0];
|
return candidates[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,6 +58,8 @@ export class BlockNavigationPolicy implements INavigationPolicy<BlockSvg> {
|
|||||||
return current.nextConnection?.targetBlock();
|
return current.nextConnection?.targetBlock();
|
||||||
} else if (current.outputConnection?.targetBlock()) {
|
} else if (current.outputConnection?.targetBlock()) {
|
||||||
return navigateBlock(current, 1);
|
return navigateBlock(current, 1);
|
||||||
|
} else if (current.getSurroundParent()) {
|
||||||
|
return navigateBlock(current.getTopStackBlock(), 1);
|
||||||
} else if (this.getParent(current) instanceof WorkspaceSvg) {
|
} else if (this.getParent(current) instanceof WorkspaceSvg) {
|
||||||
return navigateStacks(current, 1);
|
return navigateStacks(current, 1);
|
||||||
}
|
}
|
||||||
@@ -111,14 +113,27 @@ export class BlockNavigationPolicy implements INavigationPolicy<BlockSvg> {
|
|||||||
* @param block The block to retrieve the navigable children of.
|
* @param block The block to retrieve the navigable children of.
|
||||||
* @returns A list of navigable/focusable children of the given block.
|
* @returns A list of navigable/focusable children of the given block.
|
||||||
*/
|
*/
|
||||||
function getBlockNavigationCandidates(block: BlockSvg): IFocusableNode[] {
|
function getBlockNavigationCandidates(
|
||||||
|
block: BlockSvg,
|
||||||
|
forward: boolean,
|
||||||
|
): IFocusableNode[] {
|
||||||
const candidates: IFocusableNode[] = block.getIcons();
|
const candidates: IFocusableNode[] = block.getIcons();
|
||||||
|
|
||||||
for (const input of block.inputList) {
|
for (const input of block.inputList) {
|
||||||
if (!input.isVisible()) continue;
|
if (!input.isVisible()) continue;
|
||||||
candidates.push(...input.fieldRow);
|
candidates.push(...input.fieldRow);
|
||||||
if (input.connection?.targetBlock()) {
|
if (input.connection?.targetBlock()) {
|
||||||
candidates.push(input.connection.targetBlock() as BlockSvg);
|
const connectedBlock = input.connection.targetBlock() as BlockSvg;
|
||||||
|
if (input.connection.type === ConnectionType.NEXT_STATEMENT && !forward) {
|
||||||
|
const lastStackBlock = connectedBlock
|
||||||
|
.lastConnectionInStack(false)
|
||||||
|
?.getSourceBlock();
|
||||||
|
if (lastStackBlock) {
|
||||||
|
candidates.push(lastStackBlock);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
candidates.push(connectedBlock);
|
||||||
|
}
|
||||||
} else if (input.connection?.type === ConnectionType.INPUT_VALUE) {
|
} else if (input.connection?.type === ConnectionType.INPUT_VALUE) {
|
||||||
candidates.push(input.connection as RenderedConnection);
|
candidates.push(input.connection as RenderedConnection);
|
||||||
}
|
}
|
||||||
@@ -174,11 +189,11 @@ export function navigateBlock(
|
|||||||
): IFocusableNode | null {
|
): IFocusableNode | null {
|
||||||
const block =
|
const block =
|
||||||
current instanceof BlockSvg
|
current instanceof BlockSvg
|
||||||
? current.outputConnection.targetBlock()
|
? (current.outputConnection?.targetBlock() ?? current.getSurroundParent())
|
||||||
: current.getSourceBlock();
|
: current.getSourceBlock();
|
||||||
if (!(block instanceof BlockSvg)) return null;
|
if (!(block instanceof BlockSvg)) return null;
|
||||||
|
|
||||||
const candidates = getBlockNavigationCandidates(block);
|
const candidates = getBlockNavigationCandidates(block, delta > 0);
|
||||||
const currentIndex = candidates.indexOf(current);
|
const currentIndex = candidates.indexOf(current);
|
||||||
if (currentIndex === -1) return null;
|
if (currentIndex === -1) return null;
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,33 @@ suite('Cursor', function () {
|
|||||||
'tooltip': '',
|
'tooltip': '',
|
||||||
'helpUrl': '',
|
'helpUrl': '',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'type': 'multi_statement_input',
|
||||||
|
'message0': '%1 %2',
|
||||||
|
'args0': [
|
||||||
|
{
|
||||||
|
'type': 'input_statement',
|
||||||
|
'name': 'FIRST',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'type': 'input_statement',
|
||||||
|
'name': 'SECOND',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'type': 'simple_statement',
|
||||||
|
'message0': '%1',
|
||||||
|
'args0': [
|
||||||
|
{
|
||||||
|
'type': 'field_input',
|
||||||
|
'name': 'NAME',
|
||||||
|
'text': 'default',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'previousStatement': null,
|
||||||
|
'nextStatement': null,
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
this.workspace = Blockly.inject('blocklyDiv', {});
|
this.workspace = Blockly.inject('blocklyDiv', {});
|
||||||
this.cursor = this.workspace.getCursor();
|
this.cursor = this.workspace.getCursor();
|
||||||
@@ -145,6 +172,112 @@ suite('Cursor', function () {
|
|||||||
assert.equal(curNode, this.blocks.D.nextConnection);
|
assert.equal(curNode, this.blocks.D.nextConnection);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
suite('Multiple statement inputs', function () {
|
||||||
|
setup(function () {
|
||||||
|
sharedTestSetup.call(this);
|
||||||
|
Blockly.defineBlocksWithJsonArray([
|
||||||
|
{
|
||||||
|
'type': 'multi_statement_input',
|
||||||
|
'message0': '%1 %2',
|
||||||
|
'args0': [
|
||||||
|
{
|
||||||
|
'type': 'input_statement',
|
||||||
|
'name': 'FIRST',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'type': 'input_statement',
|
||||||
|
'name': 'SECOND',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'type': 'simple_statement',
|
||||||
|
'message0': '%1',
|
||||||
|
'args0': [
|
||||||
|
{
|
||||||
|
'type': 'field_input',
|
||||||
|
'name': 'NAME',
|
||||||
|
'text': 'default',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'previousStatement': null,
|
||||||
|
'nextStatement': null,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
this.workspace = Blockly.inject('blocklyDiv', {});
|
||||||
|
this.cursor = this.workspace.getCursor();
|
||||||
|
|
||||||
|
this.multiStatement1 = createRenderedBlock(
|
||||||
|
this.workspace,
|
||||||
|
'multi_statement_input',
|
||||||
|
);
|
||||||
|
this.multiStatement2 = createRenderedBlock(
|
||||||
|
this.workspace,
|
||||||
|
'multi_statement_input',
|
||||||
|
);
|
||||||
|
this.firstStatement = createRenderedBlock(
|
||||||
|
this.workspace,
|
||||||
|
'simple_statement',
|
||||||
|
);
|
||||||
|
this.secondStatement = createRenderedBlock(
|
||||||
|
this.workspace,
|
||||||
|
'simple_statement',
|
||||||
|
);
|
||||||
|
this.thirdStatement = createRenderedBlock(
|
||||||
|
this.workspace,
|
||||||
|
'simple_statement',
|
||||||
|
);
|
||||||
|
this.fourthStatement = createRenderedBlock(
|
||||||
|
this.workspace,
|
||||||
|
'simple_statement',
|
||||||
|
);
|
||||||
|
this.multiStatement1
|
||||||
|
.getInput('FIRST')
|
||||||
|
.connection.connect(this.firstStatement.previousConnection);
|
||||||
|
this.firstStatement.nextConnection.connect(
|
||||||
|
this.secondStatement.previousConnection,
|
||||||
|
);
|
||||||
|
this.multiStatement1
|
||||||
|
.getInput('SECOND')
|
||||||
|
.connection.connect(this.thirdStatement.previousConnection);
|
||||||
|
this.multiStatement2
|
||||||
|
.getInput('FIRST')
|
||||||
|
.connection.connect(this.fourthStatement.previousConnection);
|
||||||
|
});
|
||||||
|
|
||||||
|
teardown(function () {
|
||||||
|
sharedTestTeardown.call(this);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('In - from field in nested statement block to next nested statement block', function () {
|
||||||
|
this.cursor.setCurNode(this.secondStatement.getField('NAME'));
|
||||||
|
this.cursor.in();
|
||||||
|
const curNode = this.cursor.getCurNode();
|
||||||
|
assert.equal(curNode, this.thirdStatement);
|
||||||
|
});
|
||||||
|
test('In - from field in nested statement block to next stack', function () {
|
||||||
|
this.cursor.setCurNode(this.thirdStatement.getField('NAME'));
|
||||||
|
this.cursor.in();
|
||||||
|
const curNode = this.cursor.getCurNode();
|
||||||
|
assert.equal(curNode, this.multiStatement2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Out - from nested statement block to last field of previous nested statement block', function () {
|
||||||
|
this.cursor.setCurNode(this.thirdStatement);
|
||||||
|
this.cursor.out();
|
||||||
|
const curNode = this.cursor.getCurNode();
|
||||||
|
assert.equal(curNode, this.secondStatement.getField('NAME'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Out - from root block to last field of last nested statement block in previous stack', function () {
|
||||||
|
this.cursor.setCurNode(this.multiStatement2);
|
||||||
|
this.cursor.out();
|
||||||
|
const curNode = this.cursor.getCurNode();
|
||||||
|
assert.equal(curNode, this.thirdStatement.getField('NAME'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
suite('Searching', function () {
|
suite('Searching', function () {
|
||||||
setup(function () {
|
setup(function () {
|
||||||
sharedTestSetup.call(this);
|
sharedTestSetup.call(this);
|
||||||
|
|||||||
Reference in New Issue
Block a user