mirror of
https://github.com/google/blockly.git
synced 2025-12-16 06:10:12 +01:00
* refactor: Remove INavigable in favor of IFocusableNode. * chore: Fix JSDoc. * chore: Address review feedback.
190 lines
6.4 KiB
TypeScript
190 lines
6.4 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import type {BlockSvg} from '../block_svg.js';
|
|
import {ConnectionType} from '../connection_type.js';
|
|
import type {IFocusableNode} from '../interfaces/i_focusable_node.js';
|
|
import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js';
|
|
import {RenderedConnection} from '../rendered_connection.js';
|
|
|
|
/**
|
|
* Set of rules controlling keyboard navigation from a connection.
|
|
*/
|
|
export class ConnectionNavigationPolicy
|
|
implements INavigationPolicy<RenderedConnection>
|
|
{
|
|
/**
|
|
* Returns the first child of the given connection.
|
|
*
|
|
* @param current The connection to return the first child of.
|
|
* @returns The connection's first child element, or null if not none.
|
|
*/
|
|
getFirstChild(current: RenderedConnection): IFocusableNode | null {
|
|
if (current.getParentInput()) {
|
|
return current.targetConnection;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Returns the parent of the given connection.
|
|
*
|
|
* @param current The connection to return the parent of.
|
|
* @returns The given connection's parent connection or block.
|
|
*/
|
|
getParent(current: RenderedConnection): IFocusableNode | null {
|
|
if (current.type === ConnectionType.OUTPUT_VALUE) {
|
|
return current.targetConnection ?? current.getSourceBlock();
|
|
} else if (current.getParentInput()) {
|
|
return current.getSourceBlock();
|
|
}
|
|
|
|
const topBlock = current.getSourceBlock().getTopStackBlock();
|
|
return (
|
|
(this.getParentConnection(topBlock)?.targetConnection?.getParentInput()
|
|
?.connection as RenderedConnection) ?? topBlock
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Returns the next element following the given connection.
|
|
*
|
|
* @param current The connection to navigate from.
|
|
* @returns The field, input connection or block following this connection.
|
|
*/
|
|
getNextSibling(current: RenderedConnection): IFocusableNode | null {
|
|
if (current.getParentInput()) {
|
|
const parentInput = current.getParentInput();
|
|
const block = parentInput?.getSourceBlock();
|
|
if (!block || !parentInput) return null;
|
|
|
|
const curIdx = block.inputList.indexOf(parentInput);
|
|
for (let i = curIdx + 1; i < block.inputList.length; i++) {
|
|
const input = block.inputList[i];
|
|
const fieldRow = input.fieldRow;
|
|
if (fieldRow.length) return fieldRow[0];
|
|
if (input.connection) return input.connection as RenderedConnection;
|
|
}
|
|
|
|
return null;
|
|
} else if (current.type === ConnectionType.NEXT_STATEMENT) {
|
|
const nextBlock = current.targetConnection;
|
|
// If this connection is the last one in the stack, our next sibling is
|
|
// the next block stack.
|
|
const sourceBlock = current.getSourceBlock();
|
|
if (
|
|
!nextBlock &&
|
|
sourceBlock.getRootBlock().lastConnectionInStack(false) === current
|
|
) {
|
|
const topBlocks = sourceBlock.workspace.getTopBlocks(true);
|
|
let targetIndex = topBlocks.indexOf(sourceBlock.getRootBlock()) + 1;
|
|
if (targetIndex >= topBlocks.length) {
|
|
targetIndex = 0;
|
|
}
|
|
const nextBlock = topBlocks[targetIndex];
|
|
return this.getParentConnection(nextBlock) ?? nextBlock;
|
|
}
|
|
|
|
return nextBlock;
|
|
}
|
|
|
|
return current.getSourceBlock();
|
|
}
|
|
|
|
/**
|
|
* Returns the element preceding the given connection.
|
|
*
|
|
* @param current The connection to navigate from.
|
|
* @returns The field, input connection or block preceding this connection.
|
|
*/
|
|
getPreviousSibling(current: RenderedConnection): IFocusableNode | null {
|
|
if (current.getParentInput()) {
|
|
const parentInput = current.getParentInput();
|
|
const block = parentInput?.getSourceBlock();
|
|
if (!block || !parentInput) return null;
|
|
|
|
const curIdx = block.inputList.indexOf(parentInput);
|
|
for (let i = curIdx; i >= 0; i--) {
|
|
const input = block.inputList[i];
|
|
if (input.connection && input !== parentInput) {
|
|
return input.connection as RenderedConnection;
|
|
}
|
|
const fieldRow = input.fieldRow;
|
|
if (fieldRow.length) return fieldRow[fieldRow.length - 1];
|
|
}
|
|
return null;
|
|
} else if (
|
|
current.type === ConnectionType.PREVIOUS_STATEMENT ||
|
|
current.type === ConnectionType.OUTPUT_VALUE
|
|
) {
|
|
const previousConnection =
|
|
current.targetConnection && !current.targetConnection.getParentInput()
|
|
? current.targetConnection
|
|
: null;
|
|
|
|
// If this connection is a disconnected previous/output connection, our
|
|
// previous sibling is the previous block stack's last connection/block.
|
|
const sourceBlock = current.getSourceBlock();
|
|
if (
|
|
!previousConnection &&
|
|
this.getParentConnection(sourceBlock.getRootBlock()) === current
|
|
) {
|
|
const topBlocks = sourceBlock.workspace.getTopBlocks(true);
|
|
let targetIndex = topBlocks.indexOf(sourceBlock.getRootBlock()) - 1;
|
|
if (targetIndex < 0) {
|
|
targetIndex = topBlocks.length - 1;
|
|
}
|
|
const previousRootBlock = topBlocks[targetIndex];
|
|
return (
|
|
previousRootBlock.lastConnectionInStack(false) ?? previousRootBlock
|
|
);
|
|
}
|
|
|
|
return previousConnection;
|
|
} else if (current.type === ConnectionType.NEXT_STATEMENT) {
|
|
return current.getSourceBlock();
|
|
}
|
|
return 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;
|
|
}
|
|
return block.outputConnection;
|
|
}
|
|
|
|
/**
|
|
* Returns whether or not the given connection can be navigated to.
|
|
*
|
|
* @param current The instance to check for navigability.
|
|
* @returns True if the given connection can be focused.
|
|
*/
|
|
isNavigable(current: RenderedConnection): boolean {
|
|
return current.canBeFocused();
|
|
}
|
|
|
|
/**
|
|
* Returns whether the given object can be navigated from by this policy.
|
|
*
|
|
* @param current The object to check if this policy applies to.
|
|
* @returns True if the object is a RenderedConnection.
|
|
*/
|
|
isApplicable(current: any): current is RenderedConnection {
|
|
return current instanceof RenderedConnection;
|
|
}
|
|
}
|