mirror of
https://github.com/google/blockly.git
synced 2026-01-09 01:50:11 +01:00
Fix: don't visit connections with the cursor. (#9030)
This commit is contained in:
@@ -4,10 +4,11 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type {BlockSvg} from '../block_svg.js';
|
||||
import {BlockSvg} from '../block_svg.js';
|
||||
import type {Field} from '../field.js';
|
||||
import type {INavigable} from '../interfaces/i_navigable.js';
|
||||
import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js';
|
||||
import type {RenderedConnection} from '../rendered_connection.js';
|
||||
import {WorkspaceSvg} from '../workspace_svg.js';
|
||||
|
||||
/**
|
||||
* Set of rules controlling keyboard navigation from a block.
|
||||
@@ -24,7 +25,8 @@ export class BlockNavigationPolicy implements INavigationPolicy<BlockSvg> {
|
||||
for (const field of input.fieldRow) {
|
||||
return field;
|
||||
}
|
||||
if (input.connection) return input.connection as RenderedConnection;
|
||||
if (input.connection?.targetBlock())
|
||||
return input.connection.targetBlock() as BlockSvg;
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -38,12 +40,14 @@ export class BlockNavigationPolicy implements INavigationPolicy<BlockSvg> {
|
||||
* which it is attached.
|
||||
*/
|
||||
getParent(current: BlockSvg): INavigable<unknown> | null {
|
||||
const topBlock = current.getTopStackBlock();
|
||||
if (current.previousConnection?.targetBlock()) {
|
||||
const surroundParent = current.getSurroundParent();
|
||||
if (surroundParent) return surroundParent;
|
||||
} else if (current.outputConnection?.targetBlock()) {
|
||||
return current.outputConnection.targetBlock();
|
||||
}
|
||||
|
||||
return (
|
||||
(this.getParentConnection(topBlock)?.targetConnection?.getParentInput()
|
||||
?.connection as RenderedConnection) ?? topBlock
|
||||
);
|
||||
return current.workspace;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -54,21 +58,40 @@ export class BlockNavigationPolicy implements INavigationPolicy<BlockSvg> {
|
||||
* block, or its next connection.
|
||||
*/
|
||||
getNextSibling(current: BlockSvg): INavigable<unknown> | null {
|
||||
const nextConnection = current.nextConnection;
|
||||
if (!current.outputConnection?.targetConnection && !nextConnection) {
|
||||
// If this block has no connected output connection and no next
|
||||
// connection, it must be the last block in the stack, so its next sibling
|
||||
// is the first block of the next stack on the workspace.
|
||||
const topBlocks = current.workspace.getTopBlocks(true);
|
||||
let targetIndex = topBlocks.indexOf(current.getRootBlock()) + 1;
|
||||
if (targetIndex >= topBlocks.length) {
|
||||
targetIndex = 0;
|
||||
}
|
||||
const previousBlock = topBlocks[targetIndex];
|
||||
return this.getParentConnection(previousBlock) ?? previousBlock;
|
||||
if (current.nextConnection?.targetBlock()) {
|
||||
return current.nextConnection?.targetBlock();
|
||||
}
|
||||
|
||||
return nextConnection;
|
||||
const parent = this.getParent(current);
|
||||
let navigatingCrossStacks = false;
|
||||
let siblings: (BlockSvg | Field)[] = [];
|
||||
if (parent instanceof BlockSvg) {
|
||||
for (let i = 0, input; (input = parent.inputList[i]); i++) {
|
||||
if (input.connection) {
|
||||
siblings.push(...input.fieldRow);
|
||||
const child = input.connection.targetBlock();
|
||||
if (child) {
|
||||
siblings.push(child as BlockSvg);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (parent instanceof WorkspaceSvg) {
|
||||
siblings = parent.getTopBlocks(true);
|
||||
navigatingCrossStacks = true;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
const currentIndex = siblings.indexOf(
|
||||
navigatingCrossStacks ? current.getRootBlock() : current,
|
||||
);
|
||||
if (currentIndex >= 0 && currentIndex < siblings.length - 1) {
|
||||
return siblings[currentIndex + 1];
|
||||
} else if (currentIndex === siblings.length - 1 && navigatingCrossStacks) {
|
||||
return siblings[0];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -79,40 +102,45 @@ export class BlockNavigationPolicy implements INavigationPolicy<BlockSvg> {
|
||||
* connection/block of the previous block stack if it is a root block.
|
||||
*/
|
||||
getPreviousSibling(current: BlockSvg): INavigable<unknown> | null {
|
||||
const parentConnection = this.getParentConnection(current);
|
||||
if (parentConnection) return parentConnection;
|
||||
|
||||
// If this block has no output/previous connection, it must be a root block,
|
||||
// so its previous sibling is the last connection of the last block of the
|
||||
// previous stack on the workspace.
|
||||
const topBlocks = current.workspace.getTopBlocks(true);
|
||||
let targetIndex = topBlocks.indexOf(current.getRootBlock()) - 1;
|
||||
if (targetIndex < 0) {
|
||||
targetIndex = topBlocks.length - 1;
|
||||
if (current.previousConnection?.targetBlock()) {
|
||||
return current.previousConnection?.targetBlock();
|
||||
}
|
||||
|
||||
const lastBlock = topBlocks[targetIndex]
|
||||
.getDescendants(true)
|
||||
.reverse()
|
||||
.pop();
|
||||
|
||||
return lastBlock?.nextConnection ?? lastBlock ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the parent connection on a block.
|
||||
* This is either an output connection, previous connection or undefined.
|
||||
* If both connections exist return the one that is actually connected
|
||||
* to another block.
|
||||
*
|
||||
* @param block The block to find the parent connection on.
|
||||
* @returns The connection connecting to the parent of the block.
|
||||
*/
|
||||
protected getParentConnection(block: BlockSvg) {
|
||||
if (!block.outputConnection || block.previousConnection?.isConnected()) {
|
||||
return block.previousConnection;
|
||||
const parent = this.getParent(current);
|
||||
let navigatingCrossStacks = false;
|
||||
let siblings: (BlockSvg | Field)[] = [];
|
||||
if (parent instanceof BlockSvg) {
|
||||
for (let i = 0, input; (input = parent.inputList[i]); i++) {
|
||||
if (input.connection) {
|
||||
siblings.push(...input.fieldRow);
|
||||
const child = input.connection.targetBlock();
|
||||
if (child) {
|
||||
siblings.push(child as BlockSvg);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (parent instanceof WorkspaceSvg) {
|
||||
siblings = parent.getTopBlocks(true);
|
||||
navigatingCrossStacks = true;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
return block.outputConnection;
|
||||
|
||||
const currentIndex = siblings.indexOf(current);
|
||||
let result: INavigable<any> | null = null;
|
||||
if (currentIndex >= 1) {
|
||||
result = siblings[currentIndex - 1];
|
||||
} else if (currentIndex === 0 && navigatingCrossStacks) {
|
||||
result = siblings[siblings.length - 1];
|
||||
}
|
||||
|
||||
// If navigating to a previous stack, our previous sibling is the last
|
||||
// block in it.
|
||||
if (navigatingCrossStacks && result instanceof BlockSvg) {
|
||||
return result.lastConnectionInStack(false)?.getSourceBlock() ?? result;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,7 +8,6 @@ import type {BlockSvg} from '../block_svg.js';
|
||||
import type {Field} from '../field.js';
|
||||
import type {INavigable} from '../interfaces/i_navigable.js';
|
||||
import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js';
|
||||
import type {RenderedConnection} from '../rendered_connection.js';
|
||||
|
||||
/**
|
||||
* Set of rules controlling keyboard navigation from a field.
|
||||
@@ -52,8 +51,8 @@ export class FieldNavigationPolicy implements INavigationPolicy<Field<any>> {
|
||||
const fieldRow = newInput.fieldRow;
|
||||
if (fieldIdx < fieldRow.length) return fieldRow[fieldIdx];
|
||||
fieldIdx = 0;
|
||||
if (newInput.connection) {
|
||||
return newInput.connection as RenderedConnection;
|
||||
if (newInput.connection?.targetBlock()) {
|
||||
return newInput.connection.targetBlock() as BlockSvg;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@@ -74,8 +73,8 @@ export class FieldNavigationPolicy implements INavigationPolicy<Field<any>> {
|
||||
let fieldIdx = parentInput.fieldRow.indexOf(current) - 1;
|
||||
for (let i = curIdx; i >= 0; i--) {
|
||||
const input = block.inputList[i];
|
||||
if (input.connection && input !== parentInput) {
|
||||
return input.connection as RenderedConnection;
|
||||
if (input.connection?.targetBlock() && input !== parentInput) {
|
||||
return input.connection.targetBlock() as BlockSvg;
|
||||
}
|
||||
const fieldRow = input.fieldRow;
|
||||
if (fieldIdx > -1) return fieldRow[fieldIdx];
|
||||
|
||||
@@ -14,8 +14,6 @@
|
||||
*/
|
||||
|
||||
import {BlockSvg} from '../block_svg.js';
|
||||
import {ConnectionType} from '../connection_type.js';
|
||||
import {Field} from '../field.js';
|
||||
import {FieldCheckbox} from '../field_checkbox.js';
|
||||
import {FieldDropdown} from '../field_dropdown.js';
|
||||
import {FieldImage} from '../field_image.js';
|
||||
@@ -146,7 +144,12 @@ export class LineCursor extends Marker {
|
||||
}
|
||||
const newNode = this.getNextNode(
|
||||
curNode,
|
||||
this.workspace.isFlyout ? () => true : this.validLineNode.bind(this),
|
||||
(candidate: INavigable<any> | null) => {
|
||||
return (
|
||||
candidate instanceof BlockSvg &&
|
||||
!candidate.outputConnection?.targetBlock()
|
||||
);
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
@@ -168,11 +171,8 @@ export class LineCursor extends Marker {
|
||||
if (!curNode) {
|
||||
return null;
|
||||
}
|
||||
const newNode = this.getNextNode(
|
||||
curNode,
|
||||
this.workspace.isFlyout ? () => true : this.validInLineNode.bind(this),
|
||||
true,
|
||||
);
|
||||
|
||||
const newNode = this.getNextNode(curNode, () => true, true);
|
||||
|
||||
if (newNode) {
|
||||
this.setCurNode(newNode);
|
||||
@@ -193,7 +193,12 @@ export class LineCursor extends Marker {
|
||||
}
|
||||
const newNode = this.getPreviousNode(
|
||||
curNode,
|
||||
this.workspace.isFlyout ? () => true : this.validLineNode.bind(this),
|
||||
(candidate: INavigable<any> | null) => {
|
||||
return (
|
||||
candidate instanceof BlockSvg &&
|
||||
!candidate.outputConnection?.targetBlock()
|
||||
);
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
@@ -215,11 +220,8 @@ export class LineCursor extends Marker {
|
||||
if (!curNode) {
|
||||
return null;
|
||||
}
|
||||
const newNode = this.getPreviousNode(
|
||||
curNode,
|
||||
this.workspace.isFlyout ? () => true : this.validInLineNode.bind(this),
|
||||
true,
|
||||
);
|
||||
|
||||
const newNode = this.getPreviousNode(curNode, () => true, true);
|
||||
|
||||
if (newNode) {
|
||||
this.setCurNode(newNode);
|
||||
@@ -229,102 +231,26 @@ export class LineCursor extends Marker {
|
||||
|
||||
/**
|
||||
* Returns true iff the node to which we would navigate if in() were
|
||||
* called, which will be a validInLineNode, is also a validLineNode
|
||||
* - in effect, if the LineCursor is at the end of the 'current
|
||||
* called is the same as the node to which we would navigate if next() were
|
||||
* called - in effect, if the LineCursor is at the end of the 'current
|
||||
* line' of the program.
|
||||
*/
|
||||
atEndOfLine(): boolean {
|
||||
const curNode = this.getCurNode();
|
||||
if (!curNode) return false;
|
||||
const rightNode = this.getNextNode(
|
||||
const inNode = this.getNextNode(curNode, () => true, true);
|
||||
const nextNode = this.getNextNode(
|
||||
curNode,
|
||||
this.validInLineNode.bind(this),
|
||||
false,
|
||||
(candidate: INavigable<any> | null) => {
|
||||
return (
|
||||
candidate instanceof BlockSvg &&
|
||||
!candidate.outputConnection?.targetBlock()
|
||||
);
|
||||
},
|
||||
true,
|
||||
);
|
||||
return this.validLineNode(rightNode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true iff the given node represents the "beginning of a
|
||||
* new line of code" (and thus can be visited by pressing the
|
||||
* up/down arrow keys). Specifically, if the node is for:
|
||||
*
|
||||
* - Any block that is not a value block.
|
||||
* - A top-level value block (one that is unconnected).
|
||||
* - An unconnected next statement input.
|
||||
* - An unconnected 'next' connection - the "blank line at the end".
|
||||
* This is to facilitate connecting additional blocks to a
|
||||
* stack/substack.
|
||||
*
|
||||
* If options.stackConnections is true (the default) then allow the
|
||||
* cursor to visit all (useful) stack connection by additionally
|
||||
* returning true for:
|
||||
*
|
||||
* - Any next statement input
|
||||
* - Any 'next' connection.
|
||||
* - An unconnected previous statement input.
|
||||
*
|
||||
* @param node The AST node to check.
|
||||
* @returns True if the node should be visited, false otherwise.
|
||||
*/
|
||||
protected validLineNode(node: INavigable<any> | null): boolean {
|
||||
if (!node) return false;
|
||||
|
||||
if (node instanceof BlockSvg) {
|
||||
return !node.outputConnection?.isConnected();
|
||||
} else if (node instanceof RenderedConnection) {
|
||||
if (node.type === ConnectionType.NEXT_STATEMENT) {
|
||||
return this.options.stackConnections || !node.isConnected();
|
||||
} else if (node.type === ConnectionType.PREVIOUS_STATEMENT) {
|
||||
return this.options.stackConnections && !node.isConnected();
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true iff the given node can be visited by the cursor when
|
||||
* using the left/right arrow keys. Specifically, if the node is
|
||||
* any node for which validLineNode would return true, plus:
|
||||
*
|
||||
* - Any block.
|
||||
* - Any field that is not a full block field.
|
||||
* - Any unconnected next or input connection. This is to
|
||||
* facilitate connecting additional blocks.
|
||||
*
|
||||
* @param node The AST node to check whether it is valid.
|
||||
* @returns True if the node should be visited, false otherwise.
|
||||
*/
|
||||
protected validInLineNode(node: INavigable<any> | null): boolean {
|
||||
if (!node) return false;
|
||||
if (this.validLineNode(node)) return true;
|
||||
if (node instanceof BlockSvg || node instanceof Field) {
|
||||
return true;
|
||||
} else if (
|
||||
node instanceof RenderedConnection &&
|
||||
node.getParentInput() &&
|
||||
(node.type === ConnectionType.INPUT_VALUE ||
|
||||
node.type === ConnectionType.NEXT_STATEMENT)
|
||||
) {
|
||||
return !node.isConnected();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true iff the given node can be visited by the cursor.
|
||||
* Specifically, if the node is any for which validInLineNode would
|
||||
* return true, or if it is a workspace node.
|
||||
*
|
||||
* @param node The AST node to check whether it is valid.
|
||||
* @returns True if the node should be visited, false otherwise.
|
||||
*/
|
||||
protected validNode(node: INavigable<any> | null): boolean {
|
||||
return (
|
||||
!!node && (this.validInLineNode(node) || node instanceof WorkspaceSvg)
|
||||
);
|
||||
return inNode === nextNode;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -347,15 +273,15 @@ export class LineCursor extends Marker {
|
||||
let newNode =
|
||||
this.workspace.getNavigator().getFirstChild(node) ||
|
||||
this.workspace.getNavigator().getNextSibling(node);
|
||||
if (isValid(newNode)) return newNode;
|
||||
if (newNode) {
|
||||
visitedNodes.add(node);
|
||||
return this.getNextNodeImpl(newNode, isValid, visitedNodes);
|
||||
|
||||
let target = node;
|
||||
while (target && !newNode) {
|
||||
const parent = this.workspace.getNavigator().getParent(target);
|
||||
if (!parent) break;
|
||||
newNode = this.workspace.getNavigator().getNextSibling(parent);
|
||||
target = parent;
|
||||
}
|
||||
|
||||
newNode = this.findSiblingOrParentSibling(
|
||||
this.workspace.getNavigator().getParent(node),
|
||||
);
|
||||
if (isValid(newNode)) return newNode;
|
||||
if (newNode) {
|
||||
visitedNodes.add(node);
|
||||
@@ -402,15 +328,12 @@ export class LineCursor extends Marker {
|
||||
visitedNodes: Set<INavigable<any>> = new Set<INavigable<any>>(),
|
||||
): INavigable<any> | null {
|
||||
if (!node || visitedNodes.has(node)) return null;
|
||||
let newNode: INavigable<any> | null = this.workspace
|
||||
.getNavigator()
|
||||
.getPreviousSibling(node);
|
||||
|
||||
if (newNode) {
|
||||
newNode = this.getRightMostChild(newNode);
|
||||
} else {
|
||||
newNode = this.workspace.getNavigator().getParent(node);
|
||||
}
|
||||
const newNode =
|
||||
this.getRightMostChild(
|
||||
this.workspace.getNavigator().getPreviousSibling(node),
|
||||
node,
|
||||
) || this.workspace.getNavigator().getParent(node);
|
||||
|
||||
if (isValid(newNode)) return newNode;
|
||||
if (newNode) {
|
||||
@@ -441,24 +364,6 @@ export class LineCursor extends Marker {
|
||||
return this.getPreviousNodeImpl(node, isValid);
|
||||
}
|
||||
|
||||
/**
|
||||
* From the given node find either the next valid sibling or the parent's
|
||||
* next sibling.
|
||||
*
|
||||
* @param node The current position in the AST.
|
||||
* @returns The next sibling node, the parent's next sibling, or null.
|
||||
*/
|
||||
private findSiblingOrParentSibling(
|
||||
node: INavigable<any> | null,
|
||||
): INavigable<any> | null {
|
||||
if (!node) return null;
|
||||
const nextNode = this.workspace.getNavigator().getNextSibling(node);
|
||||
if (nextNode) return nextNode;
|
||||
return this.findSiblingOrParentSibling(
|
||||
this.workspace.getNavigator().getParent(node),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the right most child of a node.
|
||||
*
|
||||
@@ -466,17 +371,22 @@ export class LineCursor extends Marker {
|
||||
* @returns The right most child of the given node, or the node if no child
|
||||
* exists.
|
||||
*/
|
||||
private getRightMostChild(node: INavigable<any>): INavigable<any> | null {
|
||||
getRightMostChild(
|
||||
node: INavigable<any> | null,
|
||||
stopIfFound: INavigable<any>,
|
||||
): INavigable<any> | null {
|
||||
if (!node) return node;
|
||||
let newNode = this.workspace.getNavigator().getFirstChild(node);
|
||||
if (!newNode) return node;
|
||||
if (!newNode || newNode === stopIfFound) return node;
|
||||
for (
|
||||
let nextNode: INavigable<any> | null = newNode;
|
||||
nextNode;
|
||||
nextNode = this.workspace.getNavigator().getNextSibling(newNode)
|
||||
) {
|
||||
if (nextNode === stopIfFound) break;
|
||||
newNode = nextNode;
|
||||
}
|
||||
return this.getRightMostChild(newNode);
|
||||
return this.getRightMostChild(newNode, stopIfFound);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -537,10 +447,7 @@ export class LineCursor extends Marker {
|
||||
this.potentialNodes = null;
|
||||
if (!nodes) throw new Error('must call preDelete first');
|
||||
for (const node of nodes) {
|
||||
if (
|
||||
this.validNode(node) &&
|
||||
!this.getSourceBlockFromNode(node)?.disposed
|
||||
) {
|
||||
if (!this.getSourceBlockFromNode(node)?.disposed) {
|
||||
this.setCurNode(node);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -22,16 +22,7 @@ export class WorkspaceNavigationPolicy
|
||||
*/
|
||||
getFirstChild(current: WorkspaceSvg): INavigable<unknown> | null {
|
||||
const blocks = current.getTopBlocks(true);
|
||||
if (!blocks.length) return null;
|
||||
const block = blocks[0];
|
||||
let topConnection = block.outputConnection;
|
||||
if (
|
||||
!topConnection ||
|
||||
(block.previousConnection && block.previousConnection.isConnected())
|
||||
) {
|
||||
topConnection = block.previousConnection;
|
||||
}
|
||||
return topConnection ?? block;
|
||||
return blocks.length ? blocks[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user