mirror of
https://github.com/google/blockly.git
synced 2026-01-11 10:57:07 +01:00
refactor!: Use navigation rulesets instead of ASTNode to control keyboard navigation. (#8992)
* feat: Add interfaces for keyboard navigation. * feat: Add the Navigator. * feat: Make core types conform to INavigable. * feat: Require FlyoutItems elements to be INavigable. * feat: Add navigation policies for built-in types. * refactor: Convert Marker and LineCursor to operate on INavigables instead of ASTNodes. * chore: Delete dead code in ASTNode. * fix: Fix the tests. * chore: Assuage the linter. * fix: Fix advanced build/tests. * chore: Restore ASTNode tests. * refactor: Move isNavigable() validation into Navigator. * refactor: Exercise navigation instead of ASTNode. * chore: Rename astnode_test.js to navigation_test.js. * chore: Enable the navigation tests. * fix: Fix bug when retrieving the first child of an empty workspace.
This commit is contained in:
@@ -70,7 +70,7 @@ export class BlockFlyoutInflater implements IFlyoutInflater {
|
||||
block.getDescendants(false).forEach((b) => (b.isInFlyout = true));
|
||||
this.addBlockListeners(block);
|
||||
|
||||
return new FlyoutItem(block, BLOCK_TYPE, true);
|
||||
return new FlyoutItem(block, BLOCK_TYPE);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -48,6 +48,7 @@ import type {IDragStrategy, IDraggable} from './interfaces/i_draggable.js';
|
||||
import type {IFocusableNode} from './interfaces/i_focusable_node.js';
|
||||
import type {IFocusableTree} from './interfaces/i_focusable_tree.js';
|
||||
import {IIcon} from './interfaces/i_icon.js';
|
||||
import type {INavigable} from './interfaces/i_navigable.js';
|
||||
import * as internalConstants from './internal_constants.js';
|
||||
import {MarkerManager} from './marker_manager.js';
|
||||
import {Msg} from './msg.js';
|
||||
@@ -80,7 +81,8 @@ export class BlockSvg
|
||||
ICopyable<BlockCopyData>,
|
||||
IDraggable,
|
||||
IDeletable,
|
||||
IFocusableNode
|
||||
IFocusableNode,
|
||||
INavigable<BlockSvg>
|
||||
{
|
||||
/**
|
||||
* Constant for identifying rows that are to be rendered inline.
|
||||
@@ -1886,4 +1888,25 @@ export class BlockSvg
|
||||
common.setSelected(null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not this block can be navigated to via the keyboard.
|
||||
*
|
||||
* @returns True if this block is keyboard navigable, otherwise false.
|
||||
*/
|
||||
isNavigable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns this block's class.
|
||||
*
|
||||
* Used by keyboard navigation to look up the rules for navigating from this
|
||||
* block.
|
||||
*
|
||||
* @returns This block's class.
|
||||
*/
|
||||
getClass() {
|
||||
return BlockSvg;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -433,6 +433,16 @@ Names.prototype.populateProcedures = function (
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
export * from './interfaces/i_navigable.js';
|
||||
export * from './interfaces/i_navigation_policy.js';
|
||||
export * from './keyboard_nav/block_navigation_policy.js';
|
||||
export * from './keyboard_nav/connection_navigation_policy.js';
|
||||
export * from './keyboard_nav/field_navigation_policy.js';
|
||||
export * from './keyboard_nav/flyout_button_navigation_policy.js';
|
||||
export * from './keyboard_nav/flyout_navigation_policy.js';
|
||||
export * from './keyboard_nav/flyout_separator_navigation_policy.js';
|
||||
export * from './keyboard_nav/workspace_navigation_policy.js';
|
||||
export * from './navigator.js';
|
||||
export * from './toast.js';
|
||||
|
||||
// Re-export submodules that no longer declareLegacyNamespace.
|
||||
|
||||
@@ -33,7 +33,7 @@ export class ButtonFlyoutInflater implements IFlyoutInflater {
|
||||
);
|
||||
button.show();
|
||||
|
||||
return new FlyoutItem(button, BUTTON_TYPE, true);
|
||||
return new FlyoutItem(button, BUTTON_TYPE);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -28,6 +28,7 @@ import type {IASTNodeLocationWithBlock} from './interfaces/i_ast_node_location_w
|
||||
import type {IFocusableNode} from './interfaces/i_focusable_node.js';
|
||||
import type {IFocusableTree} from './interfaces/i_focusable_tree.js';
|
||||
import type {IKeyboardAccessible} from './interfaces/i_keyboard_accessible.js';
|
||||
import type {INavigable} from './interfaces/i_navigable.js';
|
||||
import type {IRegistrable} from './interfaces/i_registrable.js';
|
||||
import {ISerializable} from './interfaces/i_serializable.js';
|
||||
import {MarkerManager} from './marker_manager.js';
|
||||
@@ -76,7 +77,8 @@ export abstract class Field<T = any>
|
||||
IKeyboardAccessible,
|
||||
IRegistrable,
|
||||
ISerializable,
|
||||
IFocusableNode
|
||||
IFocusableNode,
|
||||
INavigable<Field<T>>
|
||||
{
|
||||
/**
|
||||
* To overwrite the default value which is set in **Field**, directly update
|
||||
@@ -1452,6 +1454,30 @@ export abstract class Field<T = any>
|
||||
`Attempted to instantiate a field from the registry that hasn't defined a 'fromJson' method.`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not this field is accessible by keyboard navigation.
|
||||
*
|
||||
* @returns True if this field is keyboard accessible, otherwise false.
|
||||
*/
|
||||
isNavigable() {
|
||||
return (
|
||||
this.isClickable() &&
|
||||
this.isCurrentlyEditable() &&
|
||||
!(this.getSourceBlock()?.isSimpleReporter() && this.isFullBlockField()) &&
|
||||
this.getParentInput().isVisible()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns this field's class.
|
||||
*
|
||||
* Used by keyboard navigation to look up the rules for navigating from this
|
||||
* field. Must be implemented by subclasses.
|
||||
*
|
||||
* @returns This field's class.
|
||||
*/
|
||||
abstract getClass(): new (...args: any) => Field<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -228,6 +228,18 @@ export class FieldCheckbox extends Field<CheckboxBool> {
|
||||
// 'override' the static fromJson method.
|
||||
return new this(options.checked, undefined, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns this field's class.
|
||||
*
|
||||
* Used by keyboard navigation to look up the rules for navigating from this
|
||||
* field.
|
||||
*
|
||||
* @returns This field's class.
|
||||
*/
|
||||
getClass() {
|
||||
return FieldCheckbox;
|
||||
}
|
||||
}
|
||||
|
||||
fieldRegistry.register('field_checkbox', FieldCheckbox);
|
||||
|
||||
@@ -796,6 +796,18 @@ export class FieldDropdown extends Field<string> {
|
||||
throw TypeError('Found invalid FieldDropdown options.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns this field's class.
|
||||
*
|
||||
* Used by keyboard navigation to look up the rules for navigating from this
|
||||
* field.
|
||||
*
|
||||
* @returns This field's class.
|
||||
*/
|
||||
getClass() {
|
||||
return FieldDropdown;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -273,6 +273,18 @@ export class FieldImage extends Field<string> {
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns this field's class.
|
||||
*
|
||||
* Used by keyboard navigation to look up the rules for navigating from this
|
||||
* field.
|
||||
*
|
||||
* @returns This field's class.
|
||||
*/
|
||||
getClass() {
|
||||
return FieldImage;
|
||||
}
|
||||
}
|
||||
|
||||
fieldRegistry.register('field_image', FieldImage);
|
||||
|
||||
@@ -126,6 +126,18 @@ export class FieldLabel extends Field<string> {
|
||||
// the static fromJson method.
|
||||
return new this(text, undefined, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns this field's class.
|
||||
*
|
||||
* Used by keyboard navigation to look up the rules for navigating from this
|
||||
* field.
|
||||
*
|
||||
* @returns This field's class.
|
||||
*/
|
||||
getClass() {
|
||||
return FieldLabel;
|
||||
}
|
||||
}
|
||||
|
||||
fieldRegistry.register('field_label', FieldLabel);
|
||||
|
||||
@@ -341,6 +341,18 @@ export class FieldNumber extends FieldInput<number> {
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns this field's class.
|
||||
*
|
||||
* Used by keyboard navigation to look up the rules for navigating from this
|
||||
* field.
|
||||
*
|
||||
* @returns This field's class.
|
||||
*/
|
||||
getClass() {
|
||||
return FieldNumber;
|
||||
}
|
||||
}
|
||||
|
||||
fieldRegistry.register('field_number', FieldNumber);
|
||||
|
||||
@@ -89,6 +89,18 @@ export class FieldTextInput extends FieldInput<string> {
|
||||
// override the static fromJson method.
|
||||
return new this(text, undefined, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns this field's class.
|
||||
*
|
||||
* Used by keyboard navigation to look up the rules for navigating from this
|
||||
* field.
|
||||
*
|
||||
* @returns This field's class.
|
||||
*/
|
||||
getClass() {
|
||||
return FieldTextInput;
|
||||
}
|
||||
}
|
||||
|
||||
fieldRegistry.register('field_input', FieldTextInput);
|
||||
|
||||
@@ -696,7 +696,6 @@ export abstract class Flyout
|
||||
this.horizontalLayout ? SeparatorAxis.X : SeparatorAxis.Y,
|
||||
),
|
||||
SEPARATOR_TYPE,
|
||||
false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import * as Css from './css.js';
|
||||
import type {IBoundedElement} from './interfaces/i_bounded_element.js';
|
||||
import type {IFocusableNode} from './interfaces/i_focusable_node.js';
|
||||
import type {IFocusableTree} from './interfaces/i_focusable_tree.js';
|
||||
import type {INavigable} from './interfaces/i_navigable.js';
|
||||
import type {IRenderedElement} from './interfaces/i_rendered_element.js';
|
||||
import {idGenerator} from './utils.js';
|
||||
import {Coordinate} from './utils/coordinate.js';
|
||||
@@ -36,7 +37,8 @@ export class FlyoutButton
|
||||
IASTNodeLocationSvg,
|
||||
IBoundedElement,
|
||||
IRenderedElement,
|
||||
IFocusableNode
|
||||
IFocusableNode,
|
||||
INavigable<FlyoutButton>
|
||||
{
|
||||
/** The horizontal margin around the text in the button. */
|
||||
static TEXT_MARGIN_X = 5;
|
||||
@@ -416,6 +418,28 @@ export class FlyoutButton
|
||||
|
||||
/** See IFocusableNode.onNodeBlur. */
|
||||
onNodeBlur(): void {}
|
||||
|
||||
/**
|
||||
* Returns whether or not this button is accessible through keyboard
|
||||
* navigation.
|
||||
*
|
||||
* @returns True if this button is keyboard accessible, otherwise false.
|
||||
*/
|
||||
isNavigable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns this button's class.
|
||||
*
|
||||
* Used by keyboard navigation to look up the rules for navigating from this
|
||||
* button.
|
||||
*
|
||||
* @returns This button's class.
|
||||
*/
|
||||
getClass() {
|
||||
return FlyoutButton;
|
||||
}
|
||||
}
|
||||
|
||||
/** CSS for buttons and labels. See css.js for use. */
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type {IBoundedElement} from './interfaces/i_bounded_element.js';
|
||||
|
||||
import type {INavigable} from './interfaces/i_navigable.js';
|
||||
/**
|
||||
* Representation of an item displayed in a flyout.
|
||||
*/
|
||||
@@ -10,13 +10,10 @@ export class FlyoutItem {
|
||||
* @param element The element that will be displayed in the flyout.
|
||||
* @param type The type of element. Should correspond to the type of the
|
||||
* flyout inflater that created this object.
|
||||
* @param focusable True if the element should be allowed to be focused by
|
||||
* e.g. keyboard navigation in the flyout.
|
||||
*/
|
||||
constructor(
|
||||
private element: IBoundedElement,
|
||||
private element: IBoundedElement & INavigable<any>,
|
||||
private type: string,
|
||||
private focusable: boolean,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -32,11 +29,4 @@ export class FlyoutItem {
|
||||
getType() {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not the flyout element can receive focus.
|
||||
*/
|
||||
isFocusable() {
|
||||
return this.focusable;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,12 +5,15 @@
|
||||
*/
|
||||
|
||||
import type {IBoundedElement} from './interfaces/i_bounded_element.js';
|
||||
import type {INavigable} from './interfaces/i_navigable.js';
|
||||
import {Rect} from './utils/rect.js';
|
||||
|
||||
/**
|
||||
* Representation of a gap between elements in a flyout.
|
||||
*/
|
||||
export class FlyoutSeparator implements IBoundedElement {
|
||||
export class FlyoutSeparator
|
||||
implements IBoundedElement, INavigable<FlyoutSeparator>
|
||||
{
|
||||
private x = 0;
|
||||
private y = 0;
|
||||
|
||||
@@ -50,6 +53,28 @@ export class FlyoutSeparator implements IBoundedElement {
|
||||
this.x += dx;
|
||||
this.y += dy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns false to prevent this separator from being navigated to by the
|
||||
* keyboard.
|
||||
*
|
||||
* @returns False.
|
||||
*/
|
||||
isNavigable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns this separator's class.
|
||||
*
|
||||
* Used by keyboard navigation to look up the rules for navigating from this
|
||||
* separator.
|
||||
*
|
||||
* @returns This separator's class.
|
||||
*/
|
||||
getClass() {
|
||||
return FlyoutSeparator;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
31
core/interfaces/i_navigable.ts
Normal file
31
core/interfaces/i_navigable.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents a UI element which can be navigated to using the keyboard.
|
||||
*/
|
||||
export interface INavigable<T> {
|
||||
/**
|
||||
* Returns whether or not this specific instance should be reachable via
|
||||
* keyboard navigation.
|
||||
*
|
||||
* Implementors should generally return true, unless there are circumstances
|
||||
* under which this item should be skipped while using keyboard navigation.
|
||||
* Common examples might include being disabled, invalid, readonly, or purely
|
||||
* a visual decoration. For example, while Fields are navigable, non-editable
|
||||
* fields return false, since they cannot be interacted with when focused.
|
||||
*
|
||||
* @returns True if this element should be included in keyboard navigation.
|
||||
*/
|
||||
isNavigable(): boolean;
|
||||
|
||||
/**
|
||||
* Returns the class of this instance.
|
||||
*
|
||||
* @returns This object's class.
|
||||
*/
|
||||
getClass(): new (...args: any) => T;
|
||||
}
|
||||
46
core/interfaces/i_navigation_policy.ts
Normal file
46
core/interfaces/i_navigation_policy.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type {INavigable} from './i_navigable.js';
|
||||
|
||||
/**
|
||||
* A set of rules that specify where keyboard navigation should proceed.
|
||||
*/
|
||||
export interface INavigationPolicy<T> {
|
||||
/**
|
||||
* Returns the first child element of the given element, if any.
|
||||
*
|
||||
* @param current The element which the user is navigating into.
|
||||
* @returns The current element's first child, or null if it has none.
|
||||
*/
|
||||
getFirstChild(current: T): INavigable<any> | null;
|
||||
|
||||
/**
|
||||
* Returns the parent element of the given element, if any.
|
||||
*
|
||||
* @param current The element which the user is navigating out of.
|
||||
* @returns The parent element of the current element, or null if it has none.
|
||||
*/
|
||||
getParent(current: T): INavigable<any> | null;
|
||||
|
||||
/**
|
||||
* Returns the peer element following the given element, if any.
|
||||
*
|
||||
* @param current The element which the user is navigating past.
|
||||
* @returns The next peer element of the current element, or null if there is
|
||||
* none.
|
||||
*/
|
||||
getNextSibling(current: T): INavigable<any> | null;
|
||||
|
||||
/**
|
||||
* Returns the peer element preceding the given element, if any.
|
||||
*
|
||||
* @param current The element which the user is navigating past.
|
||||
* @returns The previous peer element of the current element, or null if
|
||||
* there is none.
|
||||
*/
|
||||
getPreviousSibling(current: T): INavigable<any> | null;
|
||||
}
|
||||
@@ -13,18 +13,15 @@
|
||||
// Former goog.module ID: Blockly.ASTNode
|
||||
|
||||
import {Block} from '../block.js';
|
||||
import {BlockSvg} from '../block_svg.js';
|
||||
import type {Connection} from '../connection.js';
|
||||
import {ConnectionType} from '../connection_type.js';
|
||||
import type {Field} from '../field.js';
|
||||
import {FlyoutButton} from '../flyout_button.js';
|
||||
import type {FlyoutItem} from '../flyout_item.js';
|
||||
import type {Input} from '../inputs/input.js';
|
||||
import type {IASTNodeLocation} from '../interfaces/i_ast_node_location.js';
|
||||
import type {IASTNodeLocationWithBlock} from '../interfaces/i_ast_node_location_with_block.js';
|
||||
import {Coordinate} from '../utils/coordinate.js';
|
||||
import type {Workspace} from '../workspace.js';
|
||||
import {WorkspaceSvg} from '../workspace_svg.js';
|
||||
|
||||
/**
|
||||
* Class for an AST node.
|
||||
@@ -135,355 +132,6 @@ export class ASTNode {
|
||||
return block.inputList.filter((input) => input.isVisible());
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an input find the next editable field or an input with a non null
|
||||
* connection in the same block. The current location must be an input
|
||||
* connection.
|
||||
*
|
||||
* @returns The AST node holding the next field or connection or null if there
|
||||
* is no editable field or input connection after the given input.
|
||||
*/
|
||||
private findNextForInput(): ASTNode | null {
|
||||
const location = this.location as Connection;
|
||||
const parentInput = location.getParentInput();
|
||||
const block = parentInput?.getSourceBlock();
|
||||
if (!block || !parentInput) return null;
|
||||
|
||||
const visibleInputs = this.getVisibleInputs(block);
|
||||
const curIdx = visibleInputs.indexOf(parentInput);
|
||||
for (let i = curIdx + 1; i < visibleInputs.length; i++) {
|
||||
const input = visibleInputs[i];
|
||||
const fieldRow = input.fieldRow;
|
||||
for (let j = 0; j < fieldRow.length; j++) {
|
||||
const field = fieldRow[j];
|
||||
if (field.isClickable() || ASTNode.NAVIGATE_ALL_FIELDS) {
|
||||
return ASTNode.createFieldNode(field);
|
||||
}
|
||||
}
|
||||
if (input.connection) {
|
||||
return ASTNode.createInputNode(input);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a field find the next editable field or an input with a non null
|
||||
* connection in the same block. The current location must be a field.
|
||||
*
|
||||
* @returns The AST node pointing to the next field or connection or null if
|
||||
* there is no editable field or input connection after the given input.
|
||||
*/
|
||||
private findNextForField(): ASTNode | null {
|
||||
const location = this.location as Field;
|
||||
const input = location.getParentInput();
|
||||
const block = location.getSourceBlock();
|
||||
if (!block) {
|
||||
throw new Error(
|
||||
'The current AST location is not associated with a block',
|
||||
);
|
||||
}
|
||||
const visibleInputs = this.getVisibleInputs(block);
|
||||
const curIdx = visibleInputs.indexOf(input);
|
||||
let fieldIdx = input.fieldRow.indexOf(location) + 1;
|
||||
for (let i = curIdx; i < visibleInputs.length; i++) {
|
||||
const newInput = visibleInputs[i];
|
||||
const fieldRow = newInput.fieldRow;
|
||||
while (fieldIdx < fieldRow.length) {
|
||||
if (fieldRow[fieldIdx].isClickable() || ASTNode.NAVIGATE_ALL_FIELDS) {
|
||||
return ASTNode.createFieldNode(fieldRow[fieldIdx]);
|
||||
}
|
||||
fieldIdx++;
|
||||
}
|
||||
fieldIdx = 0;
|
||||
if (newInput.connection) {
|
||||
return ASTNode.createInputNode(newInput);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an input find the previous editable field or an input with a non null
|
||||
* connection in the same block. The current location must be an input
|
||||
* connection.
|
||||
*
|
||||
* @returns The AST node holding the previous field or connection.
|
||||
*/
|
||||
private findPrevForInput(): ASTNode | null {
|
||||
const location = this.location as Connection;
|
||||
const parentInput = location.getParentInput();
|
||||
const block = parentInput?.getSourceBlock();
|
||||
if (!block || !parentInput) return null;
|
||||
|
||||
const visibleInputs = this.getVisibleInputs(block);
|
||||
const curIdx = visibleInputs.indexOf(parentInput);
|
||||
for (let i = curIdx; i >= 0; i--) {
|
||||
const input = visibleInputs[i];
|
||||
if (input.connection && input !== parentInput) {
|
||||
return ASTNode.createInputNode(input);
|
||||
}
|
||||
const fieldRow = input.fieldRow;
|
||||
for (let j = fieldRow.length - 1; j >= 0; j--) {
|
||||
const field = fieldRow[j];
|
||||
if (field.isClickable() || ASTNode.NAVIGATE_ALL_FIELDS) {
|
||||
return ASTNode.createFieldNode(field);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a field find the previous editable field or an input with a non null
|
||||
* connection in the same block. The current location must be a field.
|
||||
*
|
||||
* @returns The AST node holding the previous input or field.
|
||||
*/
|
||||
private findPrevForField(): ASTNode | null {
|
||||
const location = this.location as Field;
|
||||
const parentInput = location.getParentInput();
|
||||
const block = location.getSourceBlock();
|
||||
if (!block) {
|
||||
throw new Error(
|
||||
'The current AST location is not associated with a block',
|
||||
);
|
||||
}
|
||||
const visibleInputs = this.getVisibleInputs(block);
|
||||
const curIdx = visibleInputs.indexOf(parentInput);
|
||||
let fieldIdx = parentInput.fieldRow.indexOf(location) - 1;
|
||||
for (let i = curIdx; i >= 0; i--) {
|
||||
const input = visibleInputs[i];
|
||||
if (input.connection && input !== parentInput) {
|
||||
return ASTNode.createInputNode(input);
|
||||
}
|
||||
const fieldRow = input.fieldRow;
|
||||
while (fieldIdx > -1) {
|
||||
if (fieldRow[fieldIdx].isClickable() || ASTNode.NAVIGATE_ALL_FIELDS) {
|
||||
return ASTNode.createFieldNode(fieldRow[fieldIdx]);
|
||||
}
|
||||
fieldIdx--;
|
||||
}
|
||||
// Reset the fieldIdx to the length of the field row of the previous
|
||||
// input.
|
||||
if (i - 1 >= 0) {
|
||||
fieldIdx = visibleInputs[i - 1].fieldRow.length - 1;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate between stacks of blocks on the workspace.
|
||||
*
|
||||
* @param forward True to go forward. False to go backwards.
|
||||
* @returns The first block of the next stack or null if there are no blocks
|
||||
* on the workspace.
|
||||
*/
|
||||
private navigateBetweenStacks(forward: boolean): ASTNode | null {
|
||||
let curLocation = this.getLocation();
|
||||
// TODO(#6097): Use instanceof checks to exit early for values of
|
||||
// curLocation that don't make sense.
|
||||
if ((curLocation as IASTNodeLocationWithBlock).getSourceBlock) {
|
||||
const block = (curLocation as IASTNodeLocationWithBlock).getSourceBlock();
|
||||
if (block) {
|
||||
curLocation = block;
|
||||
}
|
||||
}
|
||||
// TODO(#6097): Use instanceof checks to exit early for values of
|
||||
// curLocation that don't make sense.
|
||||
const curLocationAsBlock = curLocation as Block;
|
||||
if (!curLocationAsBlock || curLocationAsBlock.isDeadOrDying()) {
|
||||
return null;
|
||||
}
|
||||
if (curLocationAsBlock.workspace.isFlyout) {
|
||||
return this.navigateFlyoutContents(forward);
|
||||
}
|
||||
const curRoot = curLocationAsBlock.getRootBlock();
|
||||
const topBlocks = curRoot.workspace.getTopBlocks(true);
|
||||
for (let i = 0; i < topBlocks.length; i++) {
|
||||
const topBlock = topBlocks[i];
|
||||
if (curRoot.id === topBlock.id) {
|
||||
const offset = forward ? 1 : -1;
|
||||
const resultIndex = i + offset;
|
||||
if (resultIndex === -1 || resultIndex === topBlocks.length) {
|
||||
return null;
|
||||
}
|
||||
return ASTNode.createStackNode(topBlocks[resultIndex]);
|
||||
}
|
||||
}
|
||||
throw Error(
|
||||
"Couldn't find " + (forward ? 'next' : 'previous') + ' stack?!',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate between buttons and stacks of blocks on the flyout workspace.
|
||||
*
|
||||
* @param forward True to go forward. False to go backwards.
|
||||
* @returns The next button, or next stack's first block, or null
|
||||
*/
|
||||
private navigateFlyoutContents(forward: boolean): ASTNode | null {
|
||||
const nodeType = this.getType();
|
||||
let location;
|
||||
let targetWorkspace;
|
||||
|
||||
switch (nodeType) {
|
||||
case ASTNode.types.STACK: {
|
||||
location = this.getLocation() as Block;
|
||||
const workspace = location.workspace as WorkspaceSvg;
|
||||
targetWorkspace = workspace.targetWorkspace as WorkspaceSvg;
|
||||
break;
|
||||
}
|
||||
case ASTNode.types.BUTTON: {
|
||||
location = this.getLocation() as FlyoutButton;
|
||||
targetWorkspace = location.getTargetWorkspace() as WorkspaceSvg;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
const flyout = targetWorkspace.getFlyout();
|
||||
if (!flyout) return null;
|
||||
|
||||
const nextItem = this.findNextLocationInFlyout(
|
||||
flyout.getContents(),
|
||||
location,
|
||||
forward,
|
||||
);
|
||||
if (!nextItem) return null;
|
||||
|
||||
const element = nextItem.getElement();
|
||||
if (element instanceof FlyoutButton) {
|
||||
return ASTNode.createButtonNode(element);
|
||||
} else if (element instanceof BlockSvg) {
|
||||
return ASTNode.createStackNode(element);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the next (or previous if navigating backward) item in the flyout that should be navigated to.
|
||||
*
|
||||
* @param flyoutContents Contents of the current flyout.
|
||||
* @param currentLocation Current ASTNode location.
|
||||
* @param forward True if we're navigating forward, else false.
|
||||
* @returns The next (or previous) FlyoutItem, or null if there is none.
|
||||
*/
|
||||
private findNextLocationInFlyout(
|
||||
flyoutContents: FlyoutItem[],
|
||||
currentLocation: IASTNodeLocation,
|
||||
forward: boolean,
|
||||
): FlyoutItem | null {
|
||||
const currentIndex = flyoutContents.findIndex((item: FlyoutItem) => {
|
||||
if (
|
||||
currentLocation instanceof BlockSvg &&
|
||||
item.getElement() === currentLocation
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
currentLocation instanceof FlyoutButton &&
|
||||
item.getElement() === currentLocation
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (currentIndex < 0) return null;
|
||||
|
||||
let resultIndex = forward ? currentIndex + 1 : currentIndex - 1;
|
||||
// The flyout may contain non-focusable elements like spacers or custom
|
||||
// items. If the next/previous element is one of those, keep looking until a
|
||||
// focusable element is encountered.
|
||||
while (
|
||||
resultIndex >= 0 &&
|
||||
resultIndex < flyoutContents.length &&
|
||||
!flyoutContents[resultIndex].isFocusable()
|
||||
) {
|
||||
resultIndex += forward ? 1 : -1;
|
||||
}
|
||||
if (resultIndex === -1 || resultIndex === flyoutContents.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return flyoutContents[resultIndex];
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the top most AST node for a given block.
|
||||
* This is either the previous connection, output connection or block
|
||||
* depending on what kind of connections the block has.
|
||||
*
|
||||
* @param block The block that we want to find the top connection on.
|
||||
* @returns The AST node containing the top connection.
|
||||
*/
|
||||
private findTopASTNodeForBlock(block: Block): ASTNode | null {
|
||||
const topConnection = getParentConnection(block);
|
||||
if (topConnection) {
|
||||
return ASTNode.createConnectionNode(topConnection);
|
||||
} else {
|
||||
return ASTNode.createBlockNode(block);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the AST node pointing to the input that the block is nested under or if
|
||||
* the block is not nested then get the stack AST node.
|
||||
*
|
||||
* @param block The source block of the current location.
|
||||
* @returns The AST node pointing to the input connection or the top block of
|
||||
* the stack this block is in.
|
||||
*/
|
||||
private getOutAstNodeForBlock(block: Block): ASTNode | null {
|
||||
if (!block) {
|
||||
return null;
|
||||
}
|
||||
// If the block doesn't have a previous connection then it is the top of the
|
||||
// substack.
|
||||
const topBlock = block.getTopStackBlock();
|
||||
const topConnection = getParentConnection(topBlock);
|
||||
const parentInput = topConnection?.targetConnection?.getParentInput();
|
||||
// If the top connection has a parentInput, create an AST node pointing to
|
||||
// that input.
|
||||
if (parentInput) {
|
||||
return ASTNode.createInputNode(parentInput);
|
||||
} else {
|
||||
// Go to stack level if you are not underneath an input.
|
||||
return ASTNode.createStackNode(topBlock);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the first editable field or input with a connection on a given block.
|
||||
*
|
||||
* @param block The source block of the current location.
|
||||
* @returns An AST node pointing to the first field or input.
|
||||
* Null if there are no editable fields or inputs with connections on the
|
||||
* block.
|
||||
*/
|
||||
private findFirstFieldOrInput(block: Block): ASTNode | null {
|
||||
const visibleInputs = this.getVisibleInputs(block);
|
||||
for (let i = 0; i < visibleInputs.length; i++) {
|
||||
const input = visibleInputs[i];
|
||||
|
||||
const fieldRow = input.fieldRow;
|
||||
for (let j = 0; j < fieldRow.length; j++) {
|
||||
const field = fieldRow[j];
|
||||
if (field.isClickable() || ASTNode.NAVIGATE_ALL_FIELDS) {
|
||||
return ASTNode.createFieldNode(field);
|
||||
}
|
||||
}
|
||||
if (input.connection) {
|
||||
return ASTNode.createInputNode(input);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the source block of the location of this node.
|
||||
*
|
||||
@@ -504,191 +152,6 @@ export class ASTNode {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the element to the right of the current element in the AST.
|
||||
*
|
||||
* @returns An AST node that wraps the next field, connection, block, or
|
||||
* workspace. Or null if there is no node to the right.
|
||||
*/
|
||||
next(): ASTNode | null {
|
||||
switch (this.type) {
|
||||
case ASTNode.types.STACK:
|
||||
return this.navigateBetweenStacks(true);
|
||||
|
||||
case ASTNode.types.OUTPUT: {
|
||||
const connection = this.location as Connection;
|
||||
return ASTNode.createBlockNode(connection.getSourceBlock());
|
||||
}
|
||||
case ASTNode.types.FIELD:
|
||||
return this.findNextForField();
|
||||
|
||||
case ASTNode.types.INPUT:
|
||||
return this.findNextForInput();
|
||||
|
||||
case ASTNode.types.BLOCK: {
|
||||
const block = this.location as Block;
|
||||
const nextConnection = block.nextConnection;
|
||||
if (!nextConnection) return null;
|
||||
return ASTNode.createConnectionNode(nextConnection);
|
||||
}
|
||||
case ASTNode.types.PREVIOUS: {
|
||||
const connection = this.location as Connection;
|
||||
return ASTNode.createBlockNode(connection.getSourceBlock());
|
||||
}
|
||||
case ASTNode.types.NEXT: {
|
||||
const connection = this.location as Connection;
|
||||
const targetConnection = connection.targetConnection;
|
||||
return targetConnection
|
||||
? ASTNode.createConnectionNode(targetConnection)
|
||||
: null;
|
||||
}
|
||||
case ASTNode.types.BUTTON:
|
||||
return this.navigateFlyoutContents(true);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the element one level below and all the way to the left of the current
|
||||
* location.
|
||||
*
|
||||
* @returns An AST node that wraps the next field, connection, workspace, or
|
||||
* block. Or null if there is nothing below this node.
|
||||
*/
|
||||
in(): ASTNode | null {
|
||||
switch (this.type) {
|
||||
case ASTNode.types.WORKSPACE: {
|
||||
const workspace = this.location as Workspace;
|
||||
const topBlocks = workspace.getTopBlocks(true);
|
||||
if (topBlocks.length > 0) {
|
||||
return ASTNode.createStackNode(topBlocks[0]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ASTNode.types.STACK: {
|
||||
const block = this.location as Block;
|
||||
return this.findTopASTNodeForBlock(block);
|
||||
}
|
||||
case ASTNode.types.BLOCK: {
|
||||
const block = this.location as Block;
|
||||
return this.findFirstFieldOrInput(block);
|
||||
}
|
||||
case ASTNode.types.INPUT: {
|
||||
const connection = this.location as Connection;
|
||||
const targetConnection = connection.targetConnection;
|
||||
return targetConnection
|
||||
? ASTNode.createConnectionNode(targetConnection)
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the element to the left of the current element in the AST.
|
||||
*
|
||||
* @returns An AST node that wraps the previous field, connection, workspace
|
||||
* or block. Or null if no node exists to the left. null.
|
||||
*/
|
||||
prev(): ASTNode | null {
|
||||
switch (this.type) {
|
||||
case ASTNode.types.STACK:
|
||||
return this.navigateBetweenStacks(false);
|
||||
|
||||
case ASTNode.types.OUTPUT:
|
||||
return null;
|
||||
|
||||
case ASTNode.types.FIELD:
|
||||
return this.findPrevForField();
|
||||
|
||||
case ASTNode.types.INPUT:
|
||||
return this.findPrevForInput();
|
||||
|
||||
case ASTNode.types.BLOCK: {
|
||||
const block = this.location as Block;
|
||||
const topConnection = getParentConnection(block);
|
||||
if (!topConnection) return null;
|
||||
return ASTNode.createConnectionNode(topConnection);
|
||||
}
|
||||
case ASTNode.types.PREVIOUS: {
|
||||
const connection = this.location as Connection;
|
||||
const targetConnection = connection.targetConnection;
|
||||
if (targetConnection && !targetConnection.getParentInput()) {
|
||||
return ASTNode.createConnectionNode(targetConnection);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ASTNode.types.NEXT: {
|
||||
const connection = this.location as Connection;
|
||||
return ASTNode.createBlockNode(connection.getSourceBlock());
|
||||
}
|
||||
case ASTNode.types.BUTTON:
|
||||
return this.navigateFlyoutContents(false);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the next element that is one position above and all the way to the
|
||||
* left of the current location.
|
||||
*
|
||||
* @returns An AST node that wraps the next field, connection, workspace or
|
||||
* block. Or null if we are at the workspace level.
|
||||
*/
|
||||
out(): ASTNode | null {
|
||||
switch (this.type) {
|
||||
case ASTNode.types.STACK: {
|
||||
const block = this.location as Block;
|
||||
const blockPos = block.getRelativeToSurfaceXY();
|
||||
// TODO: Make sure this is in the bounds of the workspace.
|
||||
const wsCoordinate = new Coordinate(
|
||||
blockPos.x,
|
||||
blockPos.y + ASTNode.DEFAULT_OFFSET_Y,
|
||||
);
|
||||
return ASTNode.createWorkspaceNode(block.workspace, wsCoordinate);
|
||||
}
|
||||
case ASTNode.types.OUTPUT: {
|
||||
const connection = this.location as Connection;
|
||||
const target = connection.targetConnection;
|
||||
if (target) {
|
||||
return ASTNode.createConnectionNode(target);
|
||||
}
|
||||
return ASTNode.createStackNode(connection.getSourceBlock());
|
||||
}
|
||||
case ASTNode.types.FIELD: {
|
||||
const field = this.location as Field;
|
||||
const block = field.getSourceBlock();
|
||||
if (!block) {
|
||||
throw new Error(
|
||||
'The current AST location is not associated with a block',
|
||||
);
|
||||
}
|
||||
return ASTNode.createBlockNode(block);
|
||||
}
|
||||
case ASTNode.types.INPUT: {
|
||||
const connection = this.location as Connection;
|
||||
return ASTNode.createBlockNode(connection.getSourceBlock());
|
||||
}
|
||||
case ASTNode.types.BLOCK: {
|
||||
const block = this.location as Block;
|
||||
return this.getOutAstNodeForBlock(block);
|
||||
}
|
||||
case ASTNode.types.PREVIOUS: {
|
||||
const connection = this.location as Connection;
|
||||
return this.getOutAstNodeForBlock(connection.getSourceBlock());
|
||||
}
|
||||
case ASTNode.types.NEXT: {
|
||||
const connection = this.location as Connection;
|
||||
return this.getOutAstNodeForBlock(connection.getSourceBlock());
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether an AST node of the given type points to a connection.
|
||||
*
|
||||
|
||||
117
core/keyboard_nav/block_navigation_policy.ts
Normal file
117
core/keyboard_nav/block_navigation_policy.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type {BlockSvg} from '../block_svg.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 block.
|
||||
*/
|
||||
export class BlockNavigationPolicy implements INavigationPolicy<BlockSvg> {
|
||||
/**
|
||||
* Returns the first child of the given block.
|
||||
*
|
||||
* @param current The block to return the first child of.
|
||||
* @returns The first field or input of the given block, if any.
|
||||
*/
|
||||
getFirstChild(current: BlockSvg): INavigable<unknown> | null {
|
||||
for (const input of current.inputList) {
|
||||
for (const field of input.fieldRow) {
|
||||
return field;
|
||||
}
|
||||
if (input.connection) return input.connection as RenderedConnection;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent of the given block.
|
||||
*
|
||||
* @param current The block to return the parent of.
|
||||
* @returns The top block of the given block's stack, or the connection to
|
||||
* which it is attached.
|
||||
*/
|
||||
getParent(current: BlockSvg): INavigable<unknown> | null {
|
||||
const topBlock = current.getTopStackBlock();
|
||||
|
||||
return (
|
||||
(this.getParentConnection(topBlock)?.targetConnection?.getParentInput()
|
||||
?.connection as RenderedConnection) ?? topBlock
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next peer node of the given block.
|
||||
*
|
||||
* @param current The block to find the following element of.
|
||||
* @returns The first block of the next stack if the given block is a terminal
|
||||
* 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;
|
||||
}
|
||||
|
||||
return nextConnection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the previous peer node of the given block.
|
||||
*
|
||||
* @param current The block to find the preceding element of.
|
||||
* @returns The block's previous/output connection, or the last
|
||||
* 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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
return block.outputConnection;
|
||||
}
|
||||
}
|
||||
169
core/keyboard_nav/connection_navigation_policy.ts
Normal file
169
core/keyboard_nav/connection_navigation_policy.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
/**
|
||||
* @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 {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 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): INavigable<unknown> | 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): INavigable<unknown> | 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): INavigable<unknown> | 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): INavigable<unknown> | 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;
|
||||
}
|
||||
}
|
||||
91
core/keyboard_nav/field_navigation_policy.ts
Normal file
91
core/keyboard_nav/field_navigation_policy.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
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.
|
||||
*/
|
||||
export class FieldNavigationPolicy implements INavigationPolicy<Field<any>> {
|
||||
/**
|
||||
* Returns null since fields do not have children.
|
||||
*
|
||||
* @param _current The field to navigate from.
|
||||
* @returns Null.
|
||||
*/
|
||||
getFirstChild(_current: Field<any>): INavigable<unknown> | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent block of the given field.
|
||||
*
|
||||
* @param current The field to navigate from.
|
||||
* @returns The given field's parent block.
|
||||
*/
|
||||
getParent(current: Field<any>): INavigable<unknown> | null {
|
||||
return current.getSourceBlock() as BlockSvg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next field or input following the given field.
|
||||
*
|
||||
* @param current The field to navigate from.
|
||||
* @returns The next field or input in the given field's block.
|
||||
*/
|
||||
getNextSibling(current: Field<any>): INavigable<unknown> | null {
|
||||
const input = current.getParentInput();
|
||||
const block = current.getSourceBlock();
|
||||
if (!block) return null;
|
||||
|
||||
const curIdx = block.inputList.indexOf(input);
|
||||
let fieldIdx = input.fieldRow.indexOf(current) + 1;
|
||||
for (let i = curIdx; i < block.inputList.length; i++) {
|
||||
const newInput = block.inputList[i];
|
||||
const fieldRow = newInput.fieldRow;
|
||||
if (fieldIdx < fieldRow.length) return fieldRow[fieldIdx];
|
||||
fieldIdx = 0;
|
||||
if (newInput.connection) {
|
||||
return newInput.connection as RenderedConnection;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the field or input preceding the given field.
|
||||
*
|
||||
* @param current The field to navigate from.
|
||||
* @returns The preceding field or input in the given field's block.
|
||||
*/
|
||||
getPreviousSibling(current: Field<any>): INavigable<unknown> | null {
|
||||
const parentInput = current.getParentInput();
|
||||
const block = current.getSourceBlock();
|
||||
if (!block) return null;
|
||||
|
||||
const curIdx = block.inputList.indexOf(parentInput);
|
||||
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;
|
||||
}
|
||||
const fieldRow = input.fieldRow;
|
||||
if (fieldIdx > -1) return fieldRow[fieldIdx];
|
||||
|
||||
// Reset the fieldIdx to the length of the field row of the previous
|
||||
// input.
|
||||
if (i - 1 >= 0) {
|
||||
fieldIdx = block.inputList[i - 1].fieldRow.length - 1;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
56
core/keyboard_nav/flyout_button_navigation_policy.ts
Normal file
56
core/keyboard_nav/flyout_button_navigation_policy.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type {FlyoutButton} from '../flyout_button.js';
|
||||
import type {INavigable} from '../interfaces/i_navigable.js';
|
||||
import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js';
|
||||
|
||||
/**
|
||||
* Set of rules controlling keyboard navigation from a flyout button.
|
||||
*/
|
||||
export class FlyoutButtonNavigationPolicy
|
||||
implements INavigationPolicy<FlyoutButton>
|
||||
{
|
||||
/**
|
||||
* Returns null since flyout buttons have no children.
|
||||
*
|
||||
* @param _current The FlyoutButton instance to navigate from.
|
||||
* @returns Null.
|
||||
*/
|
||||
getFirstChild(_current: FlyoutButton): INavigable<unknown> | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent workspace of the given flyout button.
|
||||
*
|
||||
* @param current The FlyoutButton instance to navigate from.
|
||||
* @returns The given flyout button's parent workspace.
|
||||
*/
|
||||
getParent(current: FlyoutButton): INavigable<unknown> | null {
|
||||
return current.getWorkspace();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns null since inter-item navigation is done by FlyoutNavigationPolicy.
|
||||
*
|
||||
* @param _current The FlyoutButton instance to navigate from.
|
||||
* @returns Null.
|
||||
*/
|
||||
getNextSibling(_current: FlyoutButton): INavigable<unknown> | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns null since inter-item navigation is done by FlyoutNavigationPolicy.
|
||||
*
|
||||
* @param _current The FlyoutButton instance to navigate from.
|
||||
* @returns Null.
|
||||
*/
|
||||
getPreviousSibling(_current: FlyoutButton): INavigable<unknown> | null {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
91
core/keyboard_nav/flyout_navigation_policy.ts
Normal file
91
core/keyboard_nav/flyout_navigation_policy.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type {IFlyout} from '../interfaces/i_flyout.js';
|
||||
import type {INavigable} from '../interfaces/i_navigable.js';
|
||||
import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js';
|
||||
|
||||
/**
|
||||
* Generic navigation policy that navigates between items in the flyout.
|
||||
*/
|
||||
export class FlyoutNavigationPolicy<T> implements INavigationPolicy<T> {
|
||||
/**
|
||||
* Creates a new FlyoutNavigationPolicy instance.
|
||||
*
|
||||
* @param policy The policy to defer to for parents/children.
|
||||
* @param flyout The flyout this policy will control navigation in.
|
||||
*/
|
||||
constructor(
|
||||
private policy: INavigationPolicy<T>,
|
||||
private flyout: IFlyout,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Returns null to prevent navigating into flyout items.
|
||||
*
|
||||
* @param _current The flyout item to navigate from.
|
||||
* @returns Null to prevent navigating into flyout items.
|
||||
*/
|
||||
getFirstChild(_current: T): INavigable<unknown> | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent of the given flyout item.
|
||||
*
|
||||
* @param current The flyout item to navigate from.
|
||||
* @returns The parent of the given flyout item.
|
||||
*/
|
||||
getParent(current: T): INavigable<unknown> | null {
|
||||
return this.policy.getParent(current);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next item in the flyout relative to the given item.
|
||||
*
|
||||
* @param current The flyout item to navigate from.
|
||||
* @returns The flyout item following the given one.
|
||||
*/
|
||||
getNextSibling(current: T): INavigable<unknown> | null {
|
||||
const flyoutContents = this.flyout.getContents();
|
||||
if (!flyoutContents) return null;
|
||||
|
||||
let index = flyoutContents.findIndex(
|
||||
(flyoutItem) => flyoutItem.getElement() === current,
|
||||
);
|
||||
|
||||
if (index === -1) return null;
|
||||
index++;
|
||||
if (index >= flyoutContents.length) {
|
||||
index = 0;
|
||||
}
|
||||
|
||||
return flyoutContents[index].getElement();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the previous item in the flyout relative to the given item.
|
||||
*
|
||||
* @param current The flyout item to navigate from.
|
||||
* @returns The flyout item preceding the given one.
|
||||
*/
|
||||
getPreviousSibling(current: T): INavigable<unknown> | null {
|
||||
const flyoutContents = this.flyout.getContents();
|
||||
if (!flyoutContents) return null;
|
||||
|
||||
let index = flyoutContents.findIndex(
|
||||
(flyoutItem) => flyoutItem.getElement() === current,
|
||||
);
|
||||
|
||||
if (index === -1) return null;
|
||||
index--;
|
||||
if (index < 0) {
|
||||
index = flyoutContents.length - 1;
|
||||
}
|
||||
|
||||
return flyoutContents[index].getElement();
|
||||
}
|
||||
}
|
||||
33
core/keyboard_nav/flyout_separator_navigation_policy.ts
Normal file
33
core/keyboard_nav/flyout_separator_navigation_policy.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type {FlyoutSeparator} from '../flyout_separator.js';
|
||||
import type {INavigable} from '../interfaces/i_navigable.js';
|
||||
import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js';
|
||||
|
||||
/**
|
||||
* Set of rules controlling keyboard navigation from a flyout separator.
|
||||
* This is a no-op placeholder, since flyout separators can't be navigated to.
|
||||
*/
|
||||
export class FlyoutSeparatorNavigationPolicy
|
||||
implements INavigationPolicy<FlyoutSeparator>
|
||||
{
|
||||
getFirstChild(_current: FlyoutSeparator): INavigable<unknown> | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
getParent(_current: FlyoutSeparator): INavigable<unknown> | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
getNextSibling(_current: FlyoutSeparator): INavigable<unknown> | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
getPreviousSibling(_current: FlyoutSeparator): INavigable<unknown> | null {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -13,22 +13,37 @@
|
||||
* @author aschmiedt@google.com (Abby Schmiedt)
|
||||
*/
|
||||
|
||||
import type {Block} from '../block.js';
|
||||
import {BlockSvg} from '../block_svg.js';
|
||||
import * as common from '../common.js';
|
||||
import type {Connection} from '../connection.js';
|
||||
import {ConnectionType} from '../connection_type.js';
|
||||
import type {Field} from '../field.js';
|
||||
import {Field} from '../field.js';
|
||||
import {FieldCheckbox} from '../field_checkbox.js';
|
||||
import {FieldDropdown} from '../field_dropdown.js';
|
||||
import {FieldImage} from '../field_image.js';
|
||||
import {FieldLabel} from '../field_label.js';
|
||||
import {FieldNumber} from '../field_number.js';
|
||||
import {FieldTextInput} from '../field_textinput.js';
|
||||
import {FlyoutButton} from '../flyout_button.js';
|
||||
import {FlyoutSeparator} from '../flyout_separator.js';
|
||||
import {getFocusManager} from '../focus_manager.js';
|
||||
import {isFocusableNode} from '../interfaces/i_focusable_node.js';
|
||||
import type {INavigable} from '../interfaces/i_navigable.js';
|
||||
import * as registry from '../registry.js';
|
||||
import {RenderedConnection} from '../rendered_connection.js';
|
||||
import type {MarkerSvg} from '../renderers/common/marker_svg.js';
|
||||
import type {PathObject} from '../renderers/zelos/path_object.js';
|
||||
import {Renderer} from '../renderers/zelos/renderer.js';
|
||||
import * as dom from '../utils/dom.js';
|
||||
import type {WorkspaceSvg} from '../workspace_svg.js';
|
||||
import {WorkspaceSvg} from '../workspace_svg.js';
|
||||
import {ASTNode} from './ast_node.js';
|
||||
import {BlockNavigationPolicy} from './block_navigation_policy.js';
|
||||
import {ConnectionNavigationPolicy} from './connection_navigation_policy.js';
|
||||
import {FieldNavigationPolicy} from './field_navigation_policy.js';
|
||||
import {FlyoutButtonNavigationPolicy} from './flyout_button_navigation_policy.js';
|
||||
import {FlyoutNavigationPolicy} from './flyout_navigation_policy.js';
|
||||
import {FlyoutSeparatorNavigationPolicy} from './flyout_separator_navigation_policy.js';
|
||||
import {Marker} from './marker.js';
|
||||
import {WorkspaceNavigationPolicy} from './workspace_navigation_policy.js';
|
||||
|
||||
/** Options object for LineCursor instances. */
|
||||
export interface CursorOptions {
|
||||
@@ -54,7 +69,7 @@ export class LineCursor extends Marker {
|
||||
private readonly options: CursorOptions;
|
||||
|
||||
/** Locations to try moving the cursor to after a deletion. */
|
||||
private potentialNodes: ASTNode[] | null = null;
|
||||
private potentialNodes: INavigable<any>[] | null = null;
|
||||
|
||||
/** Whether the renderer is zelos-style. */
|
||||
private isZelos = false;
|
||||
@@ -64,7 +79,7 @@ export class LineCursor extends Marker {
|
||||
* @param options Cursor options.
|
||||
*/
|
||||
constructor(
|
||||
private readonly workspace: WorkspaceSvg,
|
||||
protected readonly workspace: WorkspaceSvg,
|
||||
options?: Partial<CursorOptions>,
|
||||
) {
|
||||
super();
|
||||
@@ -72,6 +87,54 @@ export class LineCursor extends Marker {
|
||||
this.options = {...defaultOptions, ...options};
|
||||
|
||||
this.isZelos = workspace.getRenderer() instanceof Renderer;
|
||||
|
||||
this.registerNavigationPolicies();
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers default navigation policies for Blockly's built-in types with
|
||||
* this cursor's workspace.
|
||||
*/
|
||||
protected registerNavigationPolicies() {
|
||||
const navigator = this.workspace.getNavigator();
|
||||
|
||||
const blockPolicy = new BlockNavigationPolicy();
|
||||
if (this.workspace.isFlyout) {
|
||||
const flyout = this.workspace.targetWorkspace?.getFlyout();
|
||||
if (flyout) {
|
||||
navigator.set(
|
||||
BlockSvg,
|
||||
new FlyoutNavigationPolicy(blockPolicy, flyout),
|
||||
);
|
||||
|
||||
const buttonPolicy = new FlyoutButtonNavigationPolicy();
|
||||
navigator.set(
|
||||
FlyoutButton,
|
||||
new FlyoutNavigationPolicy(buttonPolicy, flyout),
|
||||
);
|
||||
|
||||
navigator.set(
|
||||
FlyoutSeparator,
|
||||
new FlyoutNavigationPolicy(
|
||||
new FlyoutSeparatorNavigationPolicy(),
|
||||
flyout,
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
navigator.set(BlockSvg, blockPolicy);
|
||||
}
|
||||
|
||||
navigator.set(RenderedConnection, new ConnectionNavigationPolicy());
|
||||
navigator.set(WorkspaceSvg, new WorkspaceNavigationPolicy());
|
||||
|
||||
const fieldPolicy = new FieldNavigationPolicy();
|
||||
navigator.set(FieldCheckbox, fieldPolicy);
|
||||
navigator.set(FieldDropdown, fieldPolicy);
|
||||
navigator.set(FieldImage, fieldPolicy);
|
||||
navigator.set(FieldLabel, fieldPolicy);
|
||||
navigator.set(FieldNumber, fieldPolicy);
|
||||
navigator.set(FieldTextInput, fieldPolicy);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -81,14 +144,14 @@ export class LineCursor extends Marker {
|
||||
* @returns The next node, or null if the current node is
|
||||
* not set or there is no next value.
|
||||
*/
|
||||
next(): ASTNode | null {
|
||||
next(): INavigable<any> | null {
|
||||
const curNode = this.getCurNode();
|
||||
if (!curNode) {
|
||||
return null;
|
||||
}
|
||||
const newNode = this.getNextNode(
|
||||
curNode,
|
||||
this.validLineNode.bind(this),
|
||||
this.workspace.isFlyout ? () => true : this.validLineNode.bind(this),
|
||||
true,
|
||||
);
|
||||
|
||||
@@ -105,14 +168,14 @@ export class LineCursor extends Marker {
|
||||
* @returns The next node, or null if the current node is
|
||||
* not set or there is no next value.
|
||||
*/
|
||||
in(): ASTNode | null {
|
||||
in(): INavigable<any> | null {
|
||||
const curNode = this.getCurNode();
|
||||
if (!curNode) {
|
||||
return null;
|
||||
}
|
||||
const newNode = this.getNextNode(
|
||||
curNode,
|
||||
this.validInLineNode.bind(this),
|
||||
this.workspace.isFlyout ? () => true : this.validInLineNode.bind(this),
|
||||
true,
|
||||
);
|
||||
|
||||
@@ -128,14 +191,14 @@ export class LineCursor extends Marker {
|
||||
* @returns The previous node, or null if the current node
|
||||
* is not set or there is no previous value.
|
||||
*/
|
||||
prev(): ASTNode | null {
|
||||
prev(): INavigable<any> | null {
|
||||
const curNode = this.getCurNode();
|
||||
if (!curNode) {
|
||||
return null;
|
||||
}
|
||||
const newNode = this.getPreviousNode(
|
||||
curNode,
|
||||
this.validLineNode.bind(this),
|
||||
this.workspace.isFlyout ? () => true : this.validLineNode.bind(this),
|
||||
true,
|
||||
);
|
||||
|
||||
@@ -152,14 +215,14 @@ export class LineCursor extends Marker {
|
||||
* @returns The previous node, or null if the current node
|
||||
* is not set or there is no previous value.
|
||||
*/
|
||||
out(): ASTNode | null {
|
||||
out(): INavigable<any> | null {
|
||||
const curNode = this.getCurNode();
|
||||
if (!curNode) {
|
||||
return null;
|
||||
}
|
||||
const newNode = this.getPreviousNode(
|
||||
curNode,
|
||||
this.validInLineNode.bind(this),
|
||||
this.workspace.isFlyout ? () => true : this.validInLineNode.bind(this),
|
||||
true,
|
||||
);
|
||||
|
||||
@@ -209,39 +272,26 @@ export class LineCursor extends Marker {
|
||||
* @param node The AST node to check.
|
||||
* @returns True if the node should be visited, false otherwise.
|
||||
*/
|
||||
protected validLineNode(node: ASTNode | null): boolean {
|
||||
protected validLineNode(node: INavigable<any> | null): boolean {
|
||||
if (!node) return false;
|
||||
const location = node.getLocation();
|
||||
const type = node && node.getType();
|
||||
switch (type) {
|
||||
case ASTNode.types.BLOCK:
|
||||
return !(location as Block).outputConnection?.isConnected();
|
||||
case ASTNode.types.INPUT: {
|
||||
const connection = location as Connection;
|
||||
return (
|
||||
connection.type === ConnectionType.NEXT_STATEMENT &&
|
||||
(this.options.stackConnections || !connection.isConnected())
|
||||
);
|
||||
|
||||
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();
|
||||
}
|
||||
case ASTNode.types.NEXT:
|
||||
return (
|
||||
this.options.stackConnections ||
|
||||
!(location as Connection).isConnected()
|
||||
);
|
||||
case ASTNode.types.PREVIOUS:
|
||||
return (
|
||||
this.options.stackConnections &&
|
||||
!(location as Connection).isConnected()
|
||||
);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
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 valideLineNode would return true, plus:
|
||||
* any node for which validLineNode would return true, plus:
|
||||
*
|
||||
* - Any block.
|
||||
* - Any field that is not a full block field.
|
||||
@@ -251,25 +301,21 @@ export class LineCursor extends Marker {
|
||||
* @param node The AST node to check whether it is valid.
|
||||
* @returns True if the node should be visited, false otherwise.
|
||||
*/
|
||||
protected validInLineNode(node: ASTNode | null): boolean {
|
||||
protected validInLineNode(node: INavigable<any> | null): boolean {
|
||||
if (!node) return false;
|
||||
if (this.validLineNode(node)) return true;
|
||||
const location = node.getLocation();
|
||||
const type = node && node.getType();
|
||||
switch (type) {
|
||||
case ASTNode.types.BLOCK:
|
||||
return true;
|
||||
case ASTNode.types.INPUT:
|
||||
return !(location as Connection).isConnected();
|
||||
case ASTNode.types.FIELD: {
|
||||
const field = node.getLocation() as Field;
|
||||
return !(
|
||||
field.getSourceBlock()?.isSimpleReporter() && field.isFullBlockField()
|
||||
);
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -280,10 +326,9 @@ export class LineCursor extends Marker {
|
||||
* @param node The AST node to check whether it is valid.
|
||||
* @returns True if the node should be visited, false otherwise.
|
||||
*/
|
||||
protected validNode(node: ASTNode | null): boolean {
|
||||
protected validNode(node: INavigable<any> | null): boolean {
|
||||
return (
|
||||
!!node &&
|
||||
(this.validInLineNode(node) || node.getType() === ASTNode.types.WORKSPACE)
|
||||
!!node && (this.validInLineNode(node) || node instanceof WorkspaceSvg)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -295,20 +340,32 @@ export class LineCursor extends Marker {
|
||||
* @param node The current position in the AST.
|
||||
* @param isValid A function true/false depending on whether the given node
|
||||
* should be traversed.
|
||||
* @param visitedNodes A set of previously visited nodes used to avoid cycles.
|
||||
* @returns The next node in the traversal.
|
||||
*/
|
||||
private getNextNodeImpl(
|
||||
node: ASTNode | null,
|
||||
isValid: (p1: ASTNode | null) => boolean,
|
||||
): ASTNode | null {
|
||||
if (!node) return null;
|
||||
let newNode = node.in() || node.next();
|
||||
node: INavigable<any> | null,
|
||||
isValid: (p1: INavigable<any> | null) => boolean,
|
||||
visitedNodes: Set<INavigable<any>> = new Set<INavigable<any>>(),
|
||||
): INavigable<any> | null {
|
||||
if (!node || visitedNodes.has(node)) return null;
|
||||
let newNode =
|
||||
this.workspace.getNavigator().getFirstChild(node) ||
|
||||
this.workspace.getNavigator().getNextSibling(node);
|
||||
if (isValid(newNode)) return newNode;
|
||||
if (newNode) return this.getNextNodeImpl(newNode, isValid);
|
||||
if (newNode) {
|
||||
visitedNodes.add(node);
|
||||
return this.getNextNodeImpl(newNode, isValid, visitedNodes);
|
||||
}
|
||||
|
||||
newNode = this.findSiblingOrParentSibling(node.out());
|
||||
newNode = this.findSiblingOrParentSibling(
|
||||
this.workspace.getNavigator().getParent(node),
|
||||
);
|
||||
if (isValid(newNode)) return newNode;
|
||||
if (newNode) return this.getNextNodeImpl(newNode, isValid);
|
||||
if (newNode) {
|
||||
visitedNodes.add(node);
|
||||
return this.getNextNodeImpl(newNode, isValid, visitedNodes);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -323,18 +380,13 @@ export class LineCursor extends Marker {
|
||||
* @returns The next node in the traversal.
|
||||
*/
|
||||
getNextNode(
|
||||
node: ASTNode | null,
|
||||
isValid: (p1: ASTNode | null) => boolean,
|
||||
node: INavigable<any> | null,
|
||||
isValid: (p1: INavigable<any> | null) => boolean,
|
||||
loop: boolean,
|
||||
): ASTNode | null {
|
||||
if (!node) return null;
|
||||
): INavigable<any> | null {
|
||||
if (!node || (!loop && this.getLastNode() === node)) return null;
|
||||
|
||||
const potential = this.getNextNodeImpl(node, isValid);
|
||||
if (potential || !loop) return potential;
|
||||
// Loop back.
|
||||
const firstNode = this.getFirstNode();
|
||||
if (isValid(firstNode)) return firstNode;
|
||||
return this.getNextNodeImpl(firstNode, isValid);
|
||||
return this.getNextNodeImpl(node, isValid);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -345,24 +397,31 @@ export class LineCursor extends Marker {
|
||||
* @param node The current position in the AST.
|
||||
* @param isValid A function true/false depending on whether the given node
|
||||
* should be traversed.
|
||||
* @param visitedNodes A set of previously visited nodes used to avoid cycles.
|
||||
* @returns The previous node in the traversal or null if no previous node
|
||||
* exists.
|
||||
*/
|
||||
private getPreviousNodeImpl(
|
||||
node: ASTNode | null,
|
||||
isValid: (p1: ASTNode | null) => boolean,
|
||||
): ASTNode | null {
|
||||
if (!node) return null;
|
||||
let newNode: ASTNode | null = node.prev();
|
||||
node: INavigable<any> | null,
|
||||
isValid: (p1: INavigable<any> | null) => boolean,
|
||||
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 = node.out();
|
||||
newNode = this.workspace.getNavigator().getParent(node);
|
||||
}
|
||||
|
||||
if (isValid(newNode)) return newNode;
|
||||
if (newNode) return this.getPreviousNodeImpl(newNode, isValid);
|
||||
if (newNode) {
|
||||
visitedNodes.add(node);
|
||||
return this.getPreviousNodeImpl(newNode, isValid, visitedNodes);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -378,18 +437,13 @@ export class LineCursor extends Marker {
|
||||
* exists.
|
||||
*/
|
||||
getPreviousNode(
|
||||
node: ASTNode | null,
|
||||
isValid: (p1: ASTNode | null) => boolean,
|
||||
node: INavigable<any> | null,
|
||||
isValid: (p1: INavigable<any> | null) => boolean,
|
||||
loop: boolean,
|
||||
): ASTNode | null {
|
||||
if (!node) return null;
|
||||
): INavigable<any> | null {
|
||||
if (!node || (!loop && this.getFirstNode() === node)) return null;
|
||||
|
||||
const potential = this.getPreviousNodeImpl(node, isValid);
|
||||
if (potential || !loop) return potential;
|
||||
// Loop back.
|
||||
const lastNode = this.getLastNode();
|
||||
if (isValid(lastNode)) return lastNode;
|
||||
return this.getPreviousNodeImpl(lastNode, isValid);
|
||||
return this.getPreviousNodeImpl(node, isValid);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -399,11 +453,15 @@ export class LineCursor extends Marker {
|
||||
* @param node The current position in the AST.
|
||||
* @returns The next sibling node, the parent's next sibling, or null.
|
||||
*/
|
||||
private findSiblingOrParentSibling(node: ASTNode | null): ASTNode | null {
|
||||
private findSiblingOrParentSibling(
|
||||
node: INavigable<any> | null,
|
||||
): INavigable<any> | null {
|
||||
if (!node) return null;
|
||||
const nextNode = node.next();
|
||||
const nextNode = this.workspace.getNavigator().getNextSibling(node);
|
||||
if (nextNode) return nextNode;
|
||||
return this.findSiblingOrParentSibling(node.out());
|
||||
return this.findSiblingOrParentSibling(
|
||||
this.workspace.getNavigator().getParent(node),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -413,13 +471,13 @@ export class LineCursor extends Marker {
|
||||
* @returns The right most child of the given node, or the node if no child
|
||||
* exists.
|
||||
*/
|
||||
private getRightMostChild(node: ASTNode): ASTNode | null {
|
||||
let newNode = node.in();
|
||||
private getRightMostChild(node: INavigable<any>): INavigable<any> | null {
|
||||
let newNode = this.workspace.getNavigator().getFirstChild(node);
|
||||
if (!newNode) return node;
|
||||
for (
|
||||
let nextNode: ASTNode | null = newNode;
|
||||
let nextNode: INavigable<any> | null = newNode;
|
||||
nextNode;
|
||||
nextNode = newNode.next()
|
||||
nextNode = this.workspace.getNavigator().getNextSibling(newNode)
|
||||
) {
|
||||
newNode = nextNode;
|
||||
}
|
||||
@@ -448,40 +506,30 @@ export class LineCursor extends Marker {
|
||||
*
|
||||
* @param deletedBlock The block that is being deleted.
|
||||
*/
|
||||
preDelete(deletedBlock: Block) {
|
||||
preDelete(deletedBlock: BlockSvg) {
|
||||
const curNode = this.getCurNode();
|
||||
|
||||
const nodes: ASTNode[] = curNode ? [curNode] : [];
|
||||
const nodes: INavigable<any>[] = curNode ? [curNode] : [];
|
||||
// The connection to which the deleted block is attached.
|
||||
const parentConnection =
|
||||
deletedBlock.previousConnection?.targetConnection ??
|
||||
deletedBlock.outputConnection?.targetConnection;
|
||||
if (parentConnection) {
|
||||
const parentNode = ASTNode.createConnectionNode(parentConnection);
|
||||
if (parentNode) nodes.push(parentNode);
|
||||
nodes.push(parentConnection);
|
||||
}
|
||||
// The block connected to the next connection of the deleted block.
|
||||
const nextBlock = deletedBlock.getNextBlock();
|
||||
if (nextBlock) {
|
||||
const nextNode = ASTNode.createBlockNode(nextBlock);
|
||||
if (nextNode) nodes.push(nextNode);
|
||||
nodes.push(nextBlock);
|
||||
}
|
||||
// The parent block of the deleted block.
|
||||
const parentBlock = deletedBlock.getParent();
|
||||
if (parentBlock) {
|
||||
const parentNode = ASTNode.createBlockNode(parentBlock);
|
||||
if (parentNode) nodes.push(parentNode);
|
||||
nodes.push(parentBlock);
|
||||
}
|
||||
// A location on the workspace beneath the deleted block.
|
||||
// Move to the workspace.
|
||||
const curBlock = curNode?.getSourceBlock();
|
||||
if (curBlock) {
|
||||
const workspaceNode = ASTNode.createWorkspaceNode(
|
||||
this.workspace,
|
||||
curBlock.getRelativeToSurfaceXY(),
|
||||
);
|
||||
if (workspaceNode) nodes.push(workspaceNode);
|
||||
}
|
||||
nodes.push(this.workspace);
|
||||
this.potentialNodes = nodes;
|
||||
}
|
||||
|
||||
@@ -494,7 +542,10 @@ 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) && !node.getSourceBlock()?.disposed) {
|
||||
if (
|
||||
this.validNode(node) &&
|
||||
!this.toASTNode(node)?.getSourceBlock()?.disposed
|
||||
) {
|
||||
this.setCurNode(node);
|
||||
return;
|
||||
}
|
||||
@@ -513,7 +564,7 @@ export class LineCursor extends Marker {
|
||||
*
|
||||
* @returns The current field, connection, or block the cursor is on.
|
||||
*/
|
||||
override getCurNode(): ASTNode | null {
|
||||
override getCurNode(): INavigable<any> | null {
|
||||
if (!this.updateCurNodeFromFocus()) {
|
||||
// Fall back to selection if focus fails to sync. This can happen for
|
||||
// non-focusable nodes or for cases when focus may not properly propagate
|
||||
@@ -565,19 +616,17 @@ export class LineCursor extends Marker {
|
||||
*
|
||||
* @param newNode The new location of the cursor.
|
||||
*/
|
||||
override setCurNode(newNode: ASTNode | null) {
|
||||
override setCurNode(newNode: INavigable<any> | null) {
|
||||
super.setCurNode(newNode);
|
||||
|
||||
const newNodeLocation = newNode?.getLocation();
|
||||
if (isFocusableNode(newNodeLocation)) {
|
||||
getFocusManager().focusNode(newNodeLocation);
|
||||
if (isFocusableNode(newNode)) {
|
||||
getFocusManager().focusNode(newNode);
|
||||
}
|
||||
|
||||
// Try to scroll cursor into view.
|
||||
if (newNode?.getType() === ASTNode.types.BLOCK) {
|
||||
const block = newNode.getLocation() as BlockSvg;
|
||||
block.workspace.scrollBoundsIntoView(
|
||||
block.getBoundingRectangleWithoutChildren(),
|
||||
if (newNode instanceof BlockSvg) {
|
||||
newNode.workspace.scrollBoundsIntoView(
|
||||
newNode.getBoundingRectangleWithoutChildren(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -664,7 +713,7 @@ export class LineCursor extends Marker {
|
||||
*/
|
||||
private isValueInputConnection(node: ASTNode) {
|
||||
if (node?.getType() !== ASTNode.types.INPUT) return false;
|
||||
const connection = node.getLocation() as Connection;
|
||||
const connection = node.getLocation() as RenderedConnection;
|
||||
return connection.type === ConnectionType.INPUT_VALUE;
|
||||
}
|
||||
|
||||
@@ -674,7 +723,7 @@ export class LineCursor extends Marker {
|
||||
* @param node The input node to hide.
|
||||
*/
|
||||
private hideAtInput(node: ASTNode) {
|
||||
const inputConnection = node.getLocation() as Connection;
|
||||
const inputConnection = node.getLocation() as RenderedConnection;
|
||||
const sourceBlock = inputConnection.getSourceBlock() as BlockSvg;
|
||||
const input = inputConnection.getParentInput();
|
||||
if (input) {
|
||||
@@ -690,7 +739,7 @@ export class LineCursor extends Marker {
|
||||
* @param node The input node to show.
|
||||
*/
|
||||
private showAtInput(node: ASTNode) {
|
||||
const inputConnection = node.getLocation() as Connection;
|
||||
const inputConnection = node.getLocation() as RenderedConnection;
|
||||
const sourceBlock = inputConnection.getSourceBlock() as BlockSvg;
|
||||
const input = inputConnection.getParentInput();
|
||||
if (input) {
|
||||
@@ -713,7 +762,7 @@ export class LineCursor extends Marker {
|
||||
const curNode = super.getCurNode();
|
||||
const selected = common.getSelected();
|
||||
|
||||
if (selected === null && curNode?.getType() === ASTNode.types.BLOCK) {
|
||||
if (selected === null && curNode instanceof BlockSvg) {
|
||||
this.setCurNode(null);
|
||||
return;
|
||||
}
|
||||
@@ -725,17 +774,13 @@ export class LineCursor extends Marker {
|
||||
if (selected.isShadow()) {
|
||||
// OK if the current node is on the parent OR the shadow block.
|
||||
// The former happens for clicks, the latter for keyboard nav.
|
||||
if (
|
||||
curNode &&
|
||||
(curNode.getLocation() === block ||
|
||||
curNode.getLocation() === block.getParent())
|
||||
) {
|
||||
if (curNode && (curNode === block || curNode === block.getParent())) {
|
||||
return;
|
||||
}
|
||||
block = block.getParent();
|
||||
}
|
||||
if (block) {
|
||||
this.setCurNode(ASTNode.createBlockNode(block));
|
||||
this.setCurNode(block);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -752,13 +797,7 @@ export class LineCursor extends Marker {
|
||||
if (focused instanceof BlockSvg) {
|
||||
const block: BlockSvg | null = focused;
|
||||
if (block && block.workspace === this.workspace) {
|
||||
if (block.getRootBlock() === block && this.workspace.isFlyout) {
|
||||
// This block actually represents a stack. Note that this is needed
|
||||
// because ASTNode special cases stack for cross-block navigation.
|
||||
this.setCurNode(ASTNode.createStackNode(block));
|
||||
} else {
|
||||
this.setCurNode(ASTNode.createBlockNode(block));
|
||||
}
|
||||
this.setCurNode(block);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -771,10 +810,8 @@ export class LineCursor extends Marker {
|
||||
*
|
||||
* @returns The first navigable node on the workspace, or null.
|
||||
*/
|
||||
getFirstNode(): ASTNode | null {
|
||||
const topBlocks = this.workspace.getTopBlocks(true);
|
||||
if (!topBlocks.length) return null;
|
||||
return ASTNode.createTopNode(topBlocks[0]);
|
||||
getFirstNode(): INavigable<any> | null {
|
||||
return this.workspace.getNavigator().getFirstChild(this.workspace);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -782,29 +819,9 @@ export class LineCursor extends Marker {
|
||||
*
|
||||
* @returns The last navigable node on the workspace, or null.
|
||||
*/
|
||||
getLastNode(): ASTNode | null {
|
||||
// Loop back to last block if it exists.
|
||||
const topBlocks = this.workspace.getTopBlocks(true);
|
||||
if (!topBlocks.length) return null;
|
||||
|
||||
// Find the last stack.
|
||||
const lastTopBlockNode = ASTNode.createStackNode(
|
||||
topBlocks[topBlocks.length - 1],
|
||||
);
|
||||
let prevNode = lastTopBlockNode;
|
||||
let nextNode: ASTNode | null = lastTopBlockNode;
|
||||
// Iterate until you fall off the end of the stack.
|
||||
while (nextNode) {
|
||||
prevNode = nextNode;
|
||||
nextNode = this.getNextNode(
|
||||
prevNode,
|
||||
(node) => {
|
||||
return !!node;
|
||||
},
|
||||
false,
|
||||
);
|
||||
}
|
||||
return prevNode;
|
||||
getLastNode(): INavigable<any> | null {
|
||||
const first = this.getFirstNode();
|
||||
return this.getPreviousNode(first, () => true, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,8 +12,15 @@
|
||||
*/
|
||||
// Former goog.module ID: Blockly.Marker
|
||||
|
||||
import {BlockSvg} from '../block_svg.js';
|
||||
import {Field} from '../field.js';
|
||||
import {FlyoutButton} from '../flyout_button.js';
|
||||
import type {INavigable} from '../interfaces/i_navigable.js';
|
||||
import {RenderedConnection} from '../rendered_connection.js';
|
||||
import type {MarkerSvg} from '../renderers/common/marker_svg.js';
|
||||
import type {ASTNode} from './ast_node.js';
|
||||
import {Coordinate} from '../utils/coordinate.js';
|
||||
import {WorkspaceSvg} from '../workspace_svg.js';
|
||||
import {ASTNode} from './ast_node.js';
|
||||
|
||||
/**
|
||||
* Class for a marker.
|
||||
@@ -24,7 +31,7 @@ export class Marker {
|
||||
colour: string | null = null;
|
||||
|
||||
/** The current location of the marker. */
|
||||
private curNode: ASTNode | null = null;
|
||||
protected curNode: INavigable<any> | null = null;
|
||||
|
||||
/**
|
||||
* The object in charge of drawing the visual representation of the current
|
||||
@@ -58,7 +65,7 @@ export class Marker {
|
||||
*
|
||||
* @returns The current field, connection, or block the marker is on.
|
||||
*/
|
||||
getCurNode(): ASTNode | null {
|
||||
getCurNode(): INavigable<any> | null {
|
||||
return this.curNode;
|
||||
}
|
||||
|
||||
@@ -67,10 +74,10 @@ export class Marker {
|
||||
*
|
||||
* @param newNode The new location of the marker, or null to remove it.
|
||||
*/
|
||||
setCurNode(newNode: ASTNode | null) {
|
||||
setCurNode(newNode: INavigable<any> | null) {
|
||||
const oldNode = this.curNode;
|
||||
this.curNode = newNode;
|
||||
this.drawer?.draw(oldNode, this.curNode);
|
||||
this.drawer?.draw(this.toASTNode(oldNode), this.toASTNode(this.curNode));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -79,7 +86,8 @@ export class Marker {
|
||||
* @internal
|
||||
*/
|
||||
draw() {
|
||||
this.drawer?.draw(this.curNode, this.curNode);
|
||||
const node = this.toASTNode(this.curNode);
|
||||
this.drawer?.draw(node, node);
|
||||
}
|
||||
|
||||
/** Hide the marker SVG. */
|
||||
@@ -93,4 +101,46 @@ export class Marker {
|
||||
this.drawer = null;
|
||||
this.curNode = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an INavigable to a legacy ASTNode.
|
||||
*
|
||||
* @param node The INavigable instance to convert.
|
||||
* @returns An ASTNode representation of the given object if possible,
|
||||
* otherwise null.
|
||||
*/
|
||||
toASTNode(node: INavigable<any> | null): ASTNode | null {
|
||||
if (node instanceof BlockSvg) {
|
||||
return ASTNode.createBlockNode(node);
|
||||
} else if (node instanceof Field) {
|
||||
return ASTNode.createFieldNode(node);
|
||||
} else if (node instanceof WorkspaceSvg) {
|
||||
return ASTNode.createWorkspaceNode(node, new Coordinate(0, 0));
|
||||
} else if (node instanceof FlyoutButton) {
|
||||
return ASTNode.createButtonNode(node);
|
||||
} else if (node instanceof RenderedConnection) {
|
||||
return ASTNode.createConnectionNode(node);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the block that this marker's current node is a child of.
|
||||
*
|
||||
* @returns The parent block of the marker's current node if any, otherwise
|
||||
* null.
|
||||
*/
|
||||
getSourceBlock(): BlockSvg | null {
|
||||
const curNode = this.getCurNode();
|
||||
if (curNode instanceof BlockSvg) {
|
||||
return curNode;
|
||||
} else if (curNode instanceof Field) {
|
||||
return curNode.getSourceBlock() as BlockSvg;
|
||||
} else if (curNode instanceof RenderedConnection) {
|
||||
return curNode.getSourceBlock();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
66
core/keyboard_nav/workspace_navigation_policy.ts
Normal file
66
core/keyboard_nav/workspace_navigation_policy.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type {INavigable} from '../interfaces/i_navigable.js';
|
||||
import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js';
|
||||
import type {WorkspaceSvg} from '../workspace_svg.js';
|
||||
|
||||
/**
|
||||
* Set of rules controlling keyboard navigation from a workspace.
|
||||
*/
|
||||
export class WorkspaceNavigationPolicy
|
||||
implements INavigationPolicy<WorkspaceSvg>
|
||||
{
|
||||
/**
|
||||
* Returns the first child of the given workspace.
|
||||
*
|
||||
* @param current The workspace to return the first child of.
|
||||
* @returns The top block of the first block stack, if any.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent of the given workspace.
|
||||
*
|
||||
* @param _current The workspace to return the parent of.
|
||||
* @returns Null.
|
||||
*/
|
||||
getParent(_current: WorkspaceSvg): INavigable<unknown> | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next sibling of the given workspace.
|
||||
*
|
||||
* @param _current The workspace to return the next sibling of.
|
||||
* @returns Null.
|
||||
*/
|
||||
getNextSibling(_current: WorkspaceSvg): INavigable<unknown> | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the previous sibling of the given workspace.
|
||||
*
|
||||
* @param _current The workspace to return the previous sibling of.
|
||||
* @returns Null.
|
||||
*/
|
||||
getPreviousSibling(_current: WorkspaceSvg): INavigable<unknown> | null {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@ export class LabelFlyoutInflater implements IFlyoutInflater {
|
||||
);
|
||||
label.show();
|
||||
|
||||
return new FlyoutItem(label, LABEL_TYPE, true);
|
||||
return new FlyoutItem(label, LABEL_TYPE);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
106
core/navigator.ts
Normal file
106
core/navigator.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type {INavigable} from './interfaces/i_navigable.js';
|
||||
import type {INavigationPolicy} from './interfaces/i_navigation_policy.js';
|
||||
|
||||
type RuleMap<T> = Map<new (...args: any) => T, INavigationPolicy<T>>;
|
||||
|
||||
/**
|
||||
* Class responsible for determining where focus should move in response to
|
||||
* keyboard navigation commands.
|
||||
*/
|
||||
export class Navigator {
|
||||
/**
|
||||
* Map from classes to a corresponding ruleset to handle navigation from
|
||||
* instances of that class.
|
||||
*/
|
||||
private rules: RuleMap<any> = new Map();
|
||||
|
||||
/**
|
||||
* Associates a navigation ruleset with its corresponding class.
|
||||
*
|
||||
* @param key The class whose object instances should have their navigation
|
||||
* controlled by the associated policy.
|
||||
* @param policy A ruleset that determines where focus should move starting
|
||||
* from an instance of the given class.
|
||||
*/
|
||||
set<T extends INavigable<T>>(
|
||||
key: new (...args: any) => T,
|
||||
policy: INavigationPolicy<T>,
|
||||
) {
|
||||
this.rules.set(key, policy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the navigation ruleset associated with the given object instance's
|
||||
* class.
|
||||
*
|
||||
* @param key An object to retrieve a navigation ruleset for.
|
||||
* @returns The navigation ruleset of objects of the given object's class, or
|
||||
* undefined if no ruleset has been registered for the object's class.
|
||||
*/
|
||||
private get<T extends INavigable<T>>(
|
||||
key: T,
|
||||
): INavigationPolicy<T> | undefined {
|
||||
return this.rules.get(key.getClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first child of the given object instance, if any.
|
||||
*
|
||||
* @param current The object to retrieve the first child of.
|
||||
* @returns The first child node of the given object, if any.
|
||||
*/
|
||||
getFirstChild<T extends INavigable<T>>(current: T): INavigable<any> | null {
|
||||
const result = this.get(current)?.getFirstChild(current);
|
||||
if (!result) return null;
|
||||
// If the child isn't navigable, don't traverse into it; check its peers.
|
||||
if (!result.isNavigable()) return this.getNextSibling(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent of the given object instance, if any.
|
||||
*
|
||||
* @param current The object to retrieve the parent of.
|
||||
* @returns The parent node of the given object, if any.
|
||||
*/
|
||||
getParent<T extends INavigable<T>>(current: T): INavigable<any> | null {
|
||||
const result = this.get(current)?.getParent(current);
|
||||
if (!result) return null;
|
||||
if (!result.isNavigable()) return this.getParent(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next sibling of the given object instance, if any.
|
||||
*
|
||||
* @param current The object to retrieve the next sibling node of.
|
||||
* @returns The next sibling node of the given object, if any.
|
||||
*/
|
||||
getNextSibling<T extends INavigable<T>>(current: T): INavigable<any> | null {
|
||||
const result = this.get(current)?.getNextSibling(current);
|
||||
if (!result) return null;
|
||||
if (!result.isNavigable()) return this.getNextSibling(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the previous sibling of the given object instance, if any.
|
||||
*
|
||||
* @param current The object to retrieve the previous sibling node of.
|
||||
* @returns The previous sibling node of the given object, if any.
|
||||
*/
|
||||
getPreviousSibling<T extends INavigable<T>>(
|
||||
current: T,
|
||||
): INavigable<any> | null {
|
||||
const result = this.get(current)?.getPreviousSibling(current);
|
||||
if (!result) return null;
|
||||
if (!result.isNavigable()) return this.getPreviousSibling(result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import {IContextMenu} from './interfaces/i_contextmenu.js';
|
||||
import type {IFocusableNode} from './interfaces/i_focusable_node.js';
|
||||
import type {IFocusableTree} from './interfaces/i_focusable_tree.js';
|
||||
import {hasBubble} from './interfaces/i_has_bubble.js';
|
||||
import type {INavigable} from './interfaces/i_navigable.js';
|
||||
import * as internalConstants from './internal_constants.js';
|
||||
import {Coordinate} from './utils/coordinate.js';
|
||||
import * as svgMath from './utils/svg_math.js';
|
||||
@@ -38,7 +39,7 @@ const BUMP_RANDOMNESS = 10;
|
||||
*/
|
||||
export class RenderedConnection
|
||||
extends Connection
|
||||
implements IContextMenu, IFocusableNode
|
||||
implements IContextMenu, IFocusableNode, INavigable<RenderedConnection>
|
||||
{
|
||||
// TODO(b/109816955): remove '!', see go/strict-prop-init-fix.
|
||||
sourceBlock_!: BlockSvg;
|
||||
@@ -677,6 +678,24 @@ export class RenderedConnection
|
||||
| unknown
|
||||
| null as SVGElement | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not this connection is keyboard-navigable.
|
||||
*
|
||||
* @returns True.
|
||||
*/
|
||||
isNavigable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns this connection's class for keyboard navigation.
|
||||
*
|
||||
* @returns RenderedConnection.
|
||||
*/
|
||||
getClass() {
|
||||
return RenderedConnection;
|
||||
}
|
||||
}
|
||||
|
||||
export namespace RenderedConnection {
|
||||
|
||||
@@ -43,7 +43,7 @@ export class SeparatorFlyoutInflater implements IFlyoutInflater {
|
||||
? SeparatorAxis.X
|
||||
: SeparatorAxis.Y;
|
||||
const separator = new FlyoutSeparator(0, flyoutAxis);
|
||||
return new FlyoutItem(separator, SEPARATOR_TYPE, false);
|
||||
return new FlyoutItem(separator, SEPARATOR_TYPE);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -53,12 +53,14 @@ import {
|
||||
} from './interfaces/i_focusable_node.js';
|
||||
import type {IFocusableTree} from './interfaces/i_focusable_tree.js';
|
||||
import type {IMetricsManager} from './interfaces/i_metrics_manager.js';
|
||||
import type {INavigable} from './interfaces/i_navigable.js';
|
||||
import type {IToolbox} from './interfaces/i_toolbox.js';
|
||||
import type {LineCursor} from './keyboard_nav/line_cursor.js';
|
||||
import type {Marker} from './keyboard_nav/marker.js';
|
||||
import {LayerManager} from './layer_manager.js';
|
||||
import {MarkerManager} from './marker_manager.js';
|
||||
import {Msg} from './msg.js';
|
||||
import {Navigator} from './navigator.js';
|
||||
import {Options} from './options.js';
|
||||
import * as Procedures from './procedures.js';
|
||||
import * as registry from './registry.js';
|
||||
@@ -99,7 +101,12 @@ const ZOOM_TO_FIT_MARGIN = 20;
|
||||
*/
|
||||
export class WorkspaceSvg
|
||||
extends Workspace
|
||||
implements IASTNodeLocationSvg, IContextMenu, IFocusableNode, IFocusableTree
|
||||
implements
|
||||
IASTNodeLocationSvg,
|
||||
IContextMenu,
|
||||
IFocusableNode,
|
||||
IFocusableTree,
|
||||
INavigable<WorkspaceSvg>
|
||||
{
|
||||
/**
|
||||
* A wrapper function called when a resize event occurs.
|
||||
@@ -336,6 +343,12 @@ export class WorkspaceSvg
|
||||
svgBubbleCanvas_!: SVGElement;
|
||||
zoomControls_: ZoomControls | null = null;
|
||||
|
||||
/**
|
||||
* Navigator that handles moving focus between items in this workspace in
|
||||
* response to keyboard navigation commands.
|
||||
*/
|
||||
private navigator = new Navigator();
|
||||
|
||||
/**
|
||||
* @param options Dictionary of options.
|
||||
*/
|
||||
@@ -2790,6 +2803,34 @@ export class WorkspaceSvg
|
||||
if (flyout && isAutoHideable(flyout)) flyout.autoHide(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the class of this workspace.
|
||||
*
|
||||
* @returns WorkspaceSvg.
|
||||
*/
|
||||
getClass() {
|
||||
return WorkspaceSvg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not this workspace is keyboard-navigable.
|
||||
*
|
||||
* @returns True.
|
||||
*/
|
||||
isNavigable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an object responsible for coordinating movement of focus between
|
||||
* items on this workspace in response to keyboard navigation commands.
|
||||
*
|
||||
* @returns This workspace's Navigator instance.
|
||||
*/
|
||||
getNavigator(): Navigator {
|
||||
return this.navigator;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,850 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {ASTNode} from '../../build/src/core/keyboard_nav/ast_node.js';
|
||||
import {assert} from '../../node_modules/chai/chai.js';
|
||||
import {
|
||||
sharedTestSetup,
|
||||
sharedTestTeardown,
|
||||
workspaceTeardown,
|
||||
} from './test_helpers/setup_teardown.js';
|
||||
|
||||
suite('ASTNode', function () {
|
||||
setup(function () {
|
||||
sharedTestSetup.call(this);
|
||||
Blockly.defineBlocksWithJsonArray([
|
||||
{
|
||||
'type': 'input_statement',
|
||||
'message0': '%1 %2 %3 %4',
|
||||
'args0': [
|
||||
{
|
||||
'type': 'field_input',
|
||||
'name': 'NAME',
|
||||
'text': 'default',
|
||||
},
|
||||
{
|
||||
'type': 'field_input',
|
||||
'name': 'NAME',
|
||||
'text': 'default',
|
||||
},
|
||||
{
|
||||
'type': 'input_value',
|
||||
'name': 'NAME',
|
||||
},
|
||||
{
|
||||
'type': 'input_statement',
|
||||
'name': 'NAME',
|
||||
},
|
||||
],
|
||||
'previousStatement': null,
|
||||
'nextStatement': null,
|
||||
'colour': 230,
|
||||
'tooltip': '',
|
||||
'helpUrl': '',
|
||||
},
|
||||
{
|
||||
'type': 'value_input',
|
||||
'message0': '%1',
|
||||
'args0': [
|
||||
{
|
||||
'type': 'input_value',
|
||||
'name': 'NAME',
|
||||
},
|
||||
],
|
||||
'colour': 230,
|
||||
'tooltip': '',
|
||||
'helpUrl': '',
|
||||
},
|
||||
{
|
||||
'type': 'field_input',
|
||||
'message0': '%1',
|
||||
'args0': [
|
||||
{
|
||||
'type': 'field_input',
|
||||
'name': 'NAME',
|
||||
'text': 'default',
|
||||
},
|
||||
],
|
||||
'output': null,
|
||||
'colour': 230,
|
||||
'tooltip': '',
|
||||
'helpUrl': '',
|
||||
},
|
||||
]);
|
||||
this.workspace = new Blockly.Workspace();
|
||||
this.cursor = this.workspace.cursor;
|
||||
const statementInput1 = this.workspace.newBlock('input_statement');
|
||||
const statementInput2 = this.workspace.newBlock('input_statement');
|
||||
const statementInput3 = this.workspace.newBlock('input_statement');
|
||||
const statementInput4 = this.workspace.newBlock('input_statement');
|
||||
const fieldWithOutput = this.workspace.newBlock('field_input');
|
||||
const valueInput = this.workspace.newBlock('value_input');
|
||||
|
||||
statementInput1.nextConnection.connect(statementInput2.previousConnection);
|
||||
statementInput1.inputList[0].connection.connect(
|
||||
fieldWithOutput.outputConnection,
|
||||
);
|
||||
statementInput2.inputList[1].connection.connect(
|
||||
statementInput3.previousConnection,
|
||||
);
|
||||
|
||||
this.blocks = {
|
||||
statementInput1: statementInput1,
|
||||
statementInput2: statementInput2,
|
||||
statementInput3: statementInput3,
|
||||
statementInput4: statementInput4,
|
||||
fieldWithOutput: fieldWithOutput,
|
||||
valueInput: valueInput,
|
||||
};
|
||||
});
|
||||
teardown(function () {
|
||||
sharedTestTeardown.call(this);
|
||||
});
|
||||
|
||||
suite('HelperFunctions', function () {
|
||||
test('findNextForInput', function () {
|
||||
const input = this.blocks.statementInput1.inputList[0];
|
||||
const input2 = this.blocks.statementInput1.inputList[1];
|
||||
const connection = input.connection;
|
||||
const node = ASTNode.createConnectionNode(connection);
|
||||
const newASTNode = node.findNextForInput(input);
|
||||
assert.equal(newASTNode.getLocation(), input2.connection);
|
||||
});
|
||||
|
||||
test('findPrevForInput', function () {
|
||||
const input = this.blocks.statementInput1.inputList[0];
|
||||
const input2 = this.blocks.statementInput1.inputList[1];
|
||||
const connection = input2.connection;
|
||||
const node = ASTNode.createConnectionNode(connection);
|
||||
const newASTNode = node.findPrevForInput(input2);
|
||||
assert.equal(newASTNode.getLocation(), input.connection);
|
||||
});
|
||||
|
||||
test('findNextForField', function () {
|
||||
const field = this.blocks.statementInput1.inputList[0].fieldRow[0];
|
||||
const field2 = this.blocks.statementInput1.inputList[0].fieldRow[1];
|
||||
const node = ASTNode.createFieldNode(field);
|
||||
const newASTNode = node.findNextForField(field);
|
||||
assert.equal(newASTNode.getLocation(), field2);
|
||||
});
|
||||
|
||||
test('findPrevForField', function () {
|
||||
const field = this.blocks.statementInput1.inputList[0].fieldRow[0];
|
||||
const field2 = this.blocks.statementInput1.inputList[0].fieldRow[1];
|
||||
const node = ASTNode.createFieldNode(field2);
|
||||
const newASTNode = node.findPrevForField(field2);
|
||||
assert.equal(newASTNode.getLocation(), field);
|
||||
});
|
||||
|
||||
test('navigateBetweenStacks_Forward', function () {
|
||||
const node = new ASTNode(
|
||||
ASTNode.types.NEXT,
|
||||
this.blocks.statementInput1.nextConnection,
|
||||
);
|
||||
const newASTNode = node.navigateBetweenStacks(true);
|
||||
assert.equal(newASTNode.getLocation(), this.blocks.statementInput4);
|
||||
});
|
||||
|
||||
test('navigateBetweenStacks_Backward', function () {
|
||||
const node = new ASTNode(
|
||||
ASTNode.types.BLOCK,
|
||||
this.blocks.statementInput4,
|
||||
);
|
||||
const newASTNode = node.navigateBetweenStacks(false);
|
||||
assert.equal(newASTNode.getLocation(), this.blocks.statementInput1);
|
||||
});
|
||||
test('getOutAstNodeForBlock', function () {
|
||||
const node = new ASTNode(
|
||||
ASTNode.types.BLOCK,
|
||||
this.blocks.statementInput2,
|
||||
);
|
||||
const newASTNode = node.getOutAstNodeForBlock(
|
||||
this.blocks.statementInput2,
|
||||
);
|
||||
assert.equal(newASTNode.getLocation(), this.blocks.statementInput1);
|
||||
});
|
||||
test('getOutAstNodeForBlock_OneBlock', function () {
|
||||
const node = new ASTNode(
|
||||
ASTNode.types.BLOCK,
|
||||
this.blocks.statementInput4,
|
||||
);
|
||||
const newASTNode = node.getOutAstNodeForBlock(
|
||||
this.blocks.statementInput4,
|
||||
);
|
||||
assert.equal(newASTNode.getLocation(), this.blocks.statementInput4);
|
||||
});
|
||||
test('findFirstFieldOrInput_', function () {
|
||||
const node = new ASTNode(
|
||||
ASTNode.types.BLOCK,
|
||||
this.blocks.statementInput4,
|
||||
);
|
||||
const field = this.blocks.statementInput4.inputList[0].fieldRow[0];
|
||||
const newASTNode = node.findFirstFieldOrInput(
|
||||
this.blocks.statementInput4,
|
||||
);
|
||||
assert.equal(newASTNode.getLocation(), field);
|
||||
});
|
||||
});
|
||||
|
||||
suite('NavigationFunctions', function () {
|
||||
setup(function () {
|
||||
Blockly.defineBlocksWithJsonArray([
|
||||
{
|
||||
'type': 'top_connection',
|
||||
'message0': '',
|
||||
'previousStatement': null,
|
||||
'colour': 230,
|
||||
'tooltip': '',
|
||||
'helpUrl': '',
|
||||
},
|
||||
{
|
||||
'type': 'start_block',
|
||||
'message0': '',
|
||||
'nextStatement': null,
|
||||
'colour': 230,
|
||||
'tooltip': '',
|
||||
'helpUrl': '',
|
||||
},
|
||||
{
|
||||
'type': 'fields_and_input',
|
||||
'message0': '%1 hi %2 %3 %4',
|
||||
'args0': [
|
||||
{
|
||||
'type': 'field_input',
|
||||
'name': 'NAME',
|
||||
'text': 'default',
|
||||
},
|
||||
{
|
||||
'type': 'input_dummy',
|
||||
},
|
||||
{
|
||||
'type': 'field_input',
|
||||
'name': 'NAME',
|
||||
'text': 'default',
|
||||
},
|
||||
{
|
||||
'type': 'input_value',
|
||||
'name': 'NAME',
|
||||
},
|
||||
],
|
||||
'previousStatement': null,
|
||||
'nextStatement': null,
|
||||
'colour': 230,
|
||||
'tooltip': '',
|
||||
'helpUrl': '',
|
||||
},
|
||||
{
|
||||
'type': 'two_fields',
|
||||
'message0': '%1 hi',
|
||||
'args0': [
|
||||
{
|
||||
'type': 'field_input',
|
||||
'name': 'NAME',
|
||||
'text': 'default',
|
||||
},
|
||||
],
|
||||
'colour': 230,
|
||||
'tooltip': '',
|
||||
'helpUrl': '',
|
||||
},
|
||||
{
|
||||
'type': 'fields_and_input2',
|
||||
'message0': '%1 %2 %3 hi %4 bye',
|
||||
'args0': [
|
||||
{
|
||||
'type': 'input_value',
|
||||
'name': 'NAME',
|
||||
},
|
||||
{
|
||||
'type': 'field_input',
|
||||
'name': 'NAME',
|
||||
'text': 'default',
|
||||
},
|
||||
{
|
||||
'type': 'input_value',
|
||||
'name': 'NAME',
|
||||
},
|
||||
{
|
||||
'type': 'input_statement',
|
||||
'name': 'NAME',
|
||||
},
|
||||
],
|
||||
'colour': 230,
|
||||
'tooltip': '',
|
||||
'helpUrl': '',
|
||||
},
|
||||
{
|
||||
'type': 'dummy_input',
|
||||
'message0': 'Hello',
|
||||
'colour': 230,
|
||||
'tooltip': '',
|
||||
'helpUrl': '',
|
||||
},
|
||||
{
|
||||
'type': 'dummy_inputValue',
|
||||
'message0': 'Hello %1 %2',
|
||||
'args0': [
|
||||
{
|
||||
'type': 'input_dummy',
|
||||
},
|
||||
{
|
||||
'type': 'input_value',
|
||||
'name': 'NAME',
|
||||
},
|
||||
],
|
||||
'colour': 230,
|
||||
'tooltip': '',
|
||||
'helpUrl': '',
|
||||
},
|
||||
{
|
||||
'type': 'output_next',
|
||||
'message0': '',
|
||||
'output': null,
|
||||
'colour': 230,
|
||||
'tooltip': '',
|
||||
'helpUrl': '',
|
||||
'nextStatement': null,
|
||||
},
|
||||
]);
|
||||
const noNextConnection = this.workspace.newBlock('top_connection');
|
||||
const fieldAndInputs = this.workspace.newBlock('fields_and_input');
|
||||
const twoFields = this.workspace.newBlock('two_fields');
|
||||
const fieldAndInputs2 = this.workspace.newBlock('fields_and_input2');
|
||||
const noPrevConnection = this.workspace.newBlock('start_block');
|
||||
this.blocks.noNextConnection = noNextConnection;
|
||||
this.blocks.fieldAndInputs = fieldAndInputs;
|
||||
this.blocks.twoFields = twoFields;
|
||||
this.blocks.fieldAndInputs2 = fieldAndInputs2;
|
||||
this.blocks.noPrevConnection = noPrevConnection;
|
||||
|
||||
const dummyInput = this.workspace.newBlock('dummy_input');
|
||||
const dummyInputValue = this.workspace.newBlock('dummy_inputValue');
|
||||
const fieldWithOutput2 = this.workspace.newBlock('field_input');
|
||||
this.blocks.dummyInput = dummyInput;
|
||||
this.blocks.dummyInputValue = dummyInputValue;
|
||||
this.blocks.fieldWithOutput2 = fieldWithOutput2;
|
||||
|
||||
const secondBlock = this.workspace.newBlock('input_statement');
|
||||
const outputNextBlock = this.workspace.newBlock('output_next');
|
||||
this.blocks.secondBlock = secondBlock;
|
||||
this.blocks.outputNextBlock = outputNextBlock;
|
||||
});
|
||||
suite('Next', function () {
|
||||
setup(function () {
|
||||
this.singleBlockWorkspace = new Blockly.Workspace();
|
||||
const singleBlock = this.singleBlockWorkspace.newBlock('two_fields');
|
||||
this.blocks.singleBlock = singleBlock;
|
||||
});
|
||||
teardown(function () {
|
||||
workspaceTeardown.call(this, this.singleBlockWorkspace);
|
||||
});
|
||||
|
||||
test('fromPreviousToBlock', function () {
|
||||
const prevConnection = this.blocks.statementInput1.previousConnection;
|
||||
const node = ASTNode.createConnectionNode(prevConnection);
|
||||
const nextNode = node.next();
|
||||
assert.equal(nextNode.getLocation(), this.blocks.statementInput1);
|
||||
});
|
||||
test('fromBlockToNext', function () {
|
||||
const nextConnection = this.blocks.statementInput1.nextConnection;
|
||||
const node = ASTNode.createBlockNode(this.blocks.statementInput1);
|
||||
const nextNode = node.next();
|
||||
assert.equal(nextNode.getLocation(), nextConnection);
|
||||
});
|
||||
test('fromBlockToNull', function () {
|
||||
const node = ASTNode.createBlockNode(this.blocks.noNextConnection);
|
||||
const nextNode = node.next();
|
||||
assert.isNull(nextNode);
|
||||
});
|
||||
test('fromNextToPrevious', function () {
|
||||
const nextConnection = this.blocks.statementInput1.nextConnection;
|
||||
const prevConnection = this.blocks.statementInput2.previousConnection;
|
||||
const node = ASTNode.createConnectionNode(nextConnection);
|
||||
const nextNode = node.next();
|
||||
assert.equal(nextNode.getLocation(), prevConnection);
|
||||
});
|
||||
test('fromNextToNull', function () {
|
||||
const nextConnection = this.blocks.statementInput2.nextConnection;
|
||||
const node = ASTNode.createConnectionNode(nextConnection);
|
||||
const nextNode = node.next();
|
||||
assert.isNull(nextNode);
|
||||
});
|
||||
test('fromInputToInput', function () {
|
||||
const input = this.blocks.statementInput1.inputList[0];
|
||||
const inputConnection =
|
||||
this.blocks.statementInput1.inputList[1].connection;
|
||||
const node = ASTNode.createInputNode(input);
|
||||
const nextNode = node.next();
|
||||
assert.equal(nextNode.getLocation(), inputConnection);
|
||||
});
|
||||
test('fromInputToStatementInput', function () {
|
||||
const input = this.blocks.fieldAndInputs2.inputList[1];
|
||||
const inputConnection =
|
||||
this.blocks.fieldAndInputs2.inputList[2].connection;
|
||||
const node = ASTNode.createInputNode(input);
|
||||
const nextNode = node.next();
|
||||
assert.equal(nextNode.getLocation(), inputConnection);
|
||||
});
|
||||
test('fromInputToField', function () {
|
||||
const input = this.blocks.fieldAndInputs2.inputList[0];
|
||||
const field = this.blocks.fieldAndInputs2.inputList[1].fieldRow[0];
|
||||
const node = ASTNode.createInputNode(input);
|
||||
const nextNode = node.next();
|
||||
assert.equal(nextNode.getLocation(), field);
|
||||
});
|
||||
test('fromInputToNull', function () {
|
||||
const input = this.blocks.fieldAndInputs2.inputList[2];
|
||||
const node = ASTNode.createInputNode(input);
|
||||
const nextNode = node.next();
|
||||
assert.isNull(nextNode);
|
||||
});
|
||||
test('fromOutputToBlock', function () {
|
||||
const output = this.blocks.fieldWithOutput.outputConnection;
|
||||
const node = ASTNode.createConnectionNode(output);
|
||||
const nextNode = node.next();
|
||||
assert.equal(nextNode.getLocation(), this.blocks.fieldWithOutput);
|
||||
});
|
||||
test('fromFieldToInput', function () {
|
||||
const field = this.blocks.statementInput1.inputList[0].fieldRow[1];
|
||||
const inputConnection =
|
||||
this.blocks.statementInput1.inputList[0].connection;
|
||||
const node = ASTNode.createFieldNode(field);
|
||||
const nextNode = node.next();
|
||||
assert.equal(nextNode.getLocation(), inputConnection);
|
||||
});
|
||||
test('fromFieldToField', function () {
|
||||
const field = this.blocks.fieldAndInputs.inputList[0].fieldRow[0];
|
||||
const node = ASTNode.createFieldNode(field);
|
||||
const field2 = this.blocks.fieldAndInputs.inputList[1].fieldRow[0];
|
||||
const nextNode = node.next();
|
||||
assert.equal(nextNode.getLocation(), field2);
|
||||
});
|
||||
test('fromFieldToNull', function () {
|
||||
const field = this.blocks.twoFields.inputList[0].fieldRow[0];
|
||||
const node = ASTNode.createFieldNode(field);
|
||||
const nextNode = node.next();
|
||||
assert.isNull(nextNode);
|
||||
});
|
||||
test('fromStackToStack', function () {
|
||||
const node = ASTNode.createStackNode(this.blocks.statementInput1);
|
||||
const nextNode = node.next();
|
||||
assert.equal(nextNode.getLocation(), this.blocks.statementInput4);
|
||||
assert.equal(nextNode.getType(), ASTNode.types.STACK);
|
||||
});
|
||||
test('fromStackToNull', function () {
|
||||
const node = ASTNode.createStackNode(this.blocks.singleBlock);
|
||||
const nextNode = node.next();
|
||||
assert.isNull(nextNode);
|
||||
});
|
||||
});
|
||||
|
||||
suite('Previous', function () {
|
||||
test('fromPreviousToNull', function () {
|
||||
const prevConnection = this.blocks.statementInput1.previousConnection;
|
||||
const node = ASTNode.createConnectionNode(prevConnection);
|
||||
const prevNode = node.prev();
|
||||
assert.isNull(prevNode);
|
||||
});
|
||||
test('fromPreviousToNext', function () {
|
||||
const prevConnection = this.blocks.statementInput2.previousConnection;
|
||||
const node = ASTNode.createConnectionNode(prevConnection);
|
||||
const prevNode = node.prev();
|
||||
const nextConnection = this.blocks.statementInput1.nextConnection;
|
||||
assert.equal(prevNode.getLocation(), nextConnection);
|
||||
});
|
||||
test('fromPreviousToInput', function () {
|
||||
const prevConnection = this.blocks.statementInput3.previousConnection;
|
||||
const node = ASTNode.createConnectionNode(prevConnection);
|
||||
const prevNode = node.prev();
|
||||
assert.isNull(prevNode);
|
||||
});
|
||||
test('fromBlockToPrevious', function () {
|
||||
const node = ASTNode.createBlockNode(this.blocks.statementInput1);
|
||||
const prevNode = node.prev();
|
||||
const prevConnection = this.blocks.statementInput1.previousConnection;
|
||||
assert.equal(prevNode.getLocation(), prevConnection);
|
||||
});
|
||||
test('fromBlockToNull', function () {
|
||||
const node = ASTNode.createBlockNode(this.blocks.noPrevConnection);
|
||||
const prevNode = node.prev();
|
||||
assert.isNull(prevNode);
|
||||
});
|
||||
test('fromBlockToOutput', function () {
|
||||
const node = ASTNode.createBlockNode(this.blocks.fieldWithOutput);
|
||||
const prevNode = node.prev();
|
||||
const outputConnection = this.blocks.fieldWithOutput.outputConnection;
|
||||
assert.equal(prevNode.getLocation(), outputConnection);
|
||||
});
|
||||
test('fromNextToBlock', function () {
|
||||
const nextConnection = this.blocks.statementInput1.nextConnection;
|
||||
const node = ASTNode.createConnectionNode(nextConnection);
|
||||
const prevNode = node.prev();
|
||||
assert.equal(prevNode.getLocation(), this.blocks.statementInput1);
|
||||
});
|
||||
test('fromInputToField', function () {
|
||||
const input = this.blocks.statementInput1.inputList[0];
|
||||
const node = ASTNode.createInputNode(input);
|
||||
const prevNode = node.prev();
|
||||
assert.equal(prevNode.getLocation(), input.fieldRow[1]);
|
||||
});
|
||||
test('fromInputToNull', function () {
|
||||
const input = this.blocks.fieldAndInputs2.inputList[0];
|
||||
const node = ASTNode.createInputNode(input);
|
||||
const prevNode = node.prev();
|
||||
assert.isNull(prevNode);
|
||||
});
|
||||
test('fromInputToInput', function () {
|
||||
const input = this.blocks.fieldAndInputs2.inputList[2];
|
||||
const inputConnection =
|
||||
this.blocks.fieldAndInputs2.inputList[1].connection;
|
||||
const node = ASTNode.createInputNode(input);
|
||||
const prevNode = node.prev();
|
||||
assert.equal(prevNode.getLocation(), inputConnection);
|
||||
});
|
||||
test('fromOutputToNull', function () {
|
||||
const output = this.blocks.fieldWithOutput.outputConnection;
|
||||
const node = ASTNode.createConnectionNode(output);
|
||||
const prevNode = node.prev();
|
||||
assert.isNull(prevNode);
|
||||
});
|
||||
test('fromFieldToNull', function () {
|
||||
const field = this.blocks.statementInput1.inputList[0].fieldRow[0];
|
||||
const node = ASTNode.createFieldNode(field);
|
||||
const prevNode = node.prev();
|
||||
assert.isNull(prevNode);
|
||||
});
|
||||
test('fromFieldToInput', function () {
|
||||
const field = this.blocks.fieldAndInputs2.inputList[1].fieldRow[0];
|
||||
const inputConnection =
|
||||
this.blocks.fieldAndInputs2.inputList[0].connection;
|
||||
const node = ASTNode.createFieldNode(field);
|
||||
const prevNode = node.prev();
|
||||
assert.equal(prevNode.getLocation(), inputConnection);
|
||||
});
|
||||
test('fromFieldToField', function () {
|
||||
const field = this.blocks.fieldAndInputs.inputList[1].fieldRow[0];
|
||||
const field2 = this.blocks.fieldAndInputs.inputList[0].fieldRow[0];
|
||||
const node = ASTNode.createFieldNode(field);
|
||||
const prevNode = node.prev();
|
||||
assert.equal(prevNode.getLocation(), field2);
|
||||
});
|
||||
test('fromStackToStack', function () {
|
||||
const node = ASTNode.createStackNode(this.blocks.statementInput4);
|
||||
const prevNode = node.prev();
|
||||
assert.equal(prevNode.getLocation(), this.blocks.statementInput1);
|
||||
assert.equal(prevNode.getType(), ASTNode.types.STACK);
|
||||
});
|
||||
});
|
||||
|
||||
suite('In', function () {
|
||||
setup(function () {
|
||||
this.emptyWorkspace = new Blockly.Workspace();
|
||||
});
|
||||
teardown(function () {
|
||||
workspaceTeardown.call(this, this.emptyWorkspace);
|
||||
});
|
||||
|
||||
test('fromInputToOutput', function () {
|
||||
const input = this.blocks.statementInput1.inputList[0];
|
||||
const node = ASTNode.createInputNode(input);
|
||||
const inNode = node.in();
|
||||
const outputConnection = this.blocks.fieldWithOutput.outputConnection;
|
||||
assert.equal(inNode.getLocation(), outputConnection);
|
||||
});
|
||||
test('fromInputToNull', function () {
|
||||
const input = this.blocks.statementInput2.inputList[0];
|
||||
const node = ASTNode.createInputNode(input);
|
||||
const inNode = node.in();
|
||||
assert.isNull(inNode);
|
||||
});
|
||||
test('fromInputToPrevious', function () {
|
||||
const input = this.blocks.statementInput2.inputList[1];
|
||||
const previousConnection =
|
||||
this.blocks.statementInput3.previousConnection;
|
||||
const node = ASTNode.createInputNode(input);
|
||||
const inNode = node.in();
|
||||
assert.equal(inNode.getLocation(), previousConnection);
|
||||
});
|
||||
test('fromBlockToInput', function () {
|
||||
const input = this.blocks.valueInput.inputList[0];
|
||||
const node = ASTNode.createBlockNode(this.blocks.valueInput);
|
||||
const inNode = node.in();
|
||||
assert.equal(inNode.getLocation(), input.connection);
|
||||
});
|
||||
test('fromBlockToField', function () {
|
||||
const node = ASTNode.createBlockNode(this.blocks.statementInput1);
|
||||
const inNode = node.in();
|
||||
const field = this.blocks.statementInput1.inputList[0].fieldRow[0];
|
||||
assert.equal(inNode.getLocation(), field);
|
||||
});
|
||||
test('fromBlockToPrevious', function () {
|
||||
const prevConnection = this.blocks.statementInput4.previousConnection;
|
||||
const node = ASTNode.createStackNode(this.blocks.statementInput4);
|
||||
const inNode = node.in();
|
||||
assert.equal(inNode.getLocation(), prevConnection);
|
||||
assert.equal(inNode.getType(), ASTNode.types.PREVIOUS);
|
||||
});
|
||||
test('fromBlockToNull_DummyInput', function () {
|
||||
const node = ASTNode.createBlockNode(this.blocks.dummyInput);
|
||||
const inNode = node.in();
|
||||
assert.isNull(inNode);
|
||||
});
|
||||
test('fromBlockToInput_DummyInputValue', function () {
|
||||
const node = ASTNode.createBlockNode(this.blocks.dummyInputValue);
|
||||
const inputConnection =
|
||||
this.blocks.dummyInputValue.inputList[1].connection;
|
||||
const inNode = node.in();
|
||||
assert.equal(inNode.getLocation(), inputConnection);
|
||||
});
|
||||
test('fromOuputToNull', function () {
|
||||
const output = this.blocks.fieldWithOutput.outputConnection;
|
||||
const node = ASTNode.createConnectionNode(output);
|
||||
const inNode = node.in();
|
||||
assert.isNull(inNode);
|
||||
});
|
||||
test('fromFieldToNull', function () {
|
||||
const field = this.blocks.statementInput1.inputList[0].fieldRow[0];
|
||||
const node = ASTNode.createFieldNode(field);
|
||||
const inNode = node.in();
|
||||
assert.isNull(inNode);
|
||||
});
|
||||
test('fromWorkspaceToStack', function () {
|
||||
const coordinate = new Blockly.utils.Coordinate(100, 100);
|
||||
const node = ASTNode.createWorkspaceNode(this.workspace, coordinate);
|
||||
const inNode = node.in();
|
||||
assert.equal(inNode.getLocation(), this.workspace.getTopBlocks()[0]);
|
||||
assert.equal(inNode.getType(), ASTNode.types.STACK);
|
||||
});
|
||||
test('fromWorkspaceToNull', function () {
|
||||
const coordinate = new Blockly.utils.Coordinate(100, 100);
|
||||
const node = ASTNode.createWorkspaceNode(
|
||||
this.emptyWorkspace,
|
||||
coordinate,
|
||||
);
|
||||
const inNode = node.in();
|
||||
assert.isNull(inNode);
|
||||
});
|
||||
test('fromStackToPrevious', function () {
|
||||
const node = ASTNode.createStackNode(this.blocks.statementInput1);
|
||||
const previous = this.blocks.statementInput1.previousConnection;
|
||||
const inNode = node.in();
|
||||
assert.equal(inNode.getLocation(), previous);
|
||||
assert.equal(inNode.getType(), ASTNode.types.PREVIOUS);
|
||||
});
|
||||
test('fromStackToOutput', function () {
|
||||
const node = ASTNode.createStackNode(this.blocks.fieldWithOutput2);
|
||||
const output = this.blocks.fieldWithOutput2.outputConnection;
|
||||
const inNode = node.in();
|
||||
assert.equal(inNode.getLocation(), output);
|
||||
assert.equal(inNode.getType(), ASTNode.types.OUTPUT);
|
||||
});
|
||||
test('fromStackToBlock', function () {
|
||||
const node = ASTNode.createStackNode(this.blocks.dummyInput);
|
||||
const inNode = node.in();
|
||||
assert.equal(inNode.getLocation(), this.blocks.dummyInput);
|
||||
assert.equal(inNode.getType(), ASTNode.types.BLOCK);
|
||||
});
|
||||
});
|
||||
|
||||
suite('Out', function () {
|
||||
setup(function () {
|
||||
const secondBlock = this.blocks.secondBlock;
|
||||
const outputNextBlock = this.blocks.outputNextBlock;
|
||||
this.blocks.noPrevConnection.nextConnection.connect(
|
||||
secondBlock.previousConnection,
|
||||
);
|
||||
secondBlock.inputList[0].connection.connect(
|
||||
outputNextBlock.outputConnection,
|
||||
);
|
||||
});
|
||||
|
||||
test('fromInputToBlock', function () {
|
||||
const input = this.blocks.statementInput1.inputList[0];
|
||||
const node = ASTNode.createInputNode(input);
|
||||
const outNode = node.out();
|
||||
assert.equal(outNode.getType(), ASTNode.types.BLOCK);
|
||||
assert.equal(outNode.getLocation(), this.blocks.statementInput1);
|
||||
});
|
||||
test('fromOutputToInput', function () {
|
||||
const output = this.blocks.fieldWithOutput.outputConnection;
|
||||
const node = ASTNode.createConnectionNode(output);
|
||||
const outNode = node.out();
|
||||
assert.equal(outNode.getType(), ASTNode.types.INPUT);
|
||||
assert.equal(
|
||||
outNode.getLocation(),
|
||||
this.blocks.statementInput1.inputList[0].connection,
|
||||
);
|
||||
});
|
||||
test('fromOutputToStack', function () {
|
||||
const output = this.blocks.fieldWithOutput2.outputConnection;
|
||||
const node = ASTNode.createConnectionNode(output);
|
||||
const outNode = node.out();
|
||||
assert.equal(outNode.getType(), ASTNode.types.STACK);
|
||||
assert.equal(outNode.getLocation(), this.blocks.fieldWithOutput2);
|
||||
});
|
||||
test('fromFieldToBlock', function () {
|
||||
const field = this.blocks.statementInput1.inputList[0].fieldRow[0];
|
||||
const node = ASTNode.createFieldNode(field);
|
||||
const outNode = node.out();
|
||||
assert.equal(outNode.getType(), ASTNode.types.BLOCK);
|
||||
assert.equal(outNode.getLocation(), this.blocks.statementInput1);
|
||||
});
|
||||
test('fromStackToWorkspace', function () {
|
||||
const stub = sinon
|
||||
.stub(this.blocks.statementInput4, 'getRelativeToSurfaceXY')
|
||||
.returns({x: 10, y: 10});
|
||||
const node = ASTNode.createStackNode(this.blocks.statementInput4);
|
||||
const outNode = node.out();
|
||||
assert.equal(outNode.getType(), ASTNode.types.WORKSPACE);
|
||||
assert.equal(outNode.wsCoordinate.x, 10);
|
||||
assert.equal(outNode.wsCoordinate.y, -10);
|
||||
stub.restore();
|
||||
});
|
||||
test('fromPreviousToInput', function () {
|
||||
const previous = this.blocks.statementInput3.previousConnection;
|
||||
const inputConnection =
|
||||
this.blocks.statementInput2.inputList[1].connection;
|
||||
const node = ASTNode.createConnectionNode(previous);
|
||||
const outNode = node.out();
|
||||
assert.equal(outNode.getType(), ASTNode.types.INPUT);
|
||||
assert.equal(outNode.getLocation(), inputConnection);
|
||||
});
|
||||
test('fromPreviousToStack', function () {
|
||||
const previous = this.blocks.statementInput2.previousConnection;
|
||||
const node = ASTNode.createConnectionNode(previous);
|
||||
const outNode = node.out();
|
||||
assert.equal(outNode.getType(), ASTNode.types.STACK);
|
||||
assert.equal(outNode.getLocation(), this.blocks.statementInput1);
|
||||
});
|
||||
test('fromNextToInput', function () {
|
||||
const next = this.blocks.statementInput3.nextConnection;
|
||||
const inputConnection =
|
||||
this.blocks.statementInput2.inputList[1].connection;
|
||||
const node = ASTNode.createConnectionNode(next);
|
||||
const outNode = node.out();
|
||||
assert.equal(outNode.getType(), ASTNode.types.INPUT);
|
||||
assert.equal(outNode.getLocation(), inputConnection);
|
||||
});
|
||||
test('fromNextToStack', function () {
|
||||
const next = this.blocks.statementInput2.nextConnection;
|
||||
const node = ASTNode.createConnectionNode(next);
|
||||
const outNode = node.out();
|
||||
assert.equal(outNode.getType(), ASTNode.types.STACK);
|
||||
assert.equal(outNode.getLocation(), this.blocks.statementInput1);
|
||||
});
|
||||
test('fromNextToStack_NoPreviousConnection', function () {
|
||||
const next = this.blocks.secondBlock.nextConnection;
|
||||
const node = ASTNode.createConnectionNode(next);
|
||||
const outNode = node.out();
|
||||
assert.equal(outNode.getType(), ASTNode.types.STACK);
|
||||
assert.equal(outNode.getLocation(), this.blocks.noPrevConnection);
|
||||
});
|
||||
/**
|
||||
* This is where there is a block with both an output connection and a
|
||||
* next connection attached to an input.
|
||||
*/
|
||||
test('fromNextToInput_OutputAndPreviousConnection', function () {
|
||||
const next = this.blocks.outputNextBlock.nextConnection;
|
||||
const node = ASTNode.createConnectionNode(next);
|
||||
const outNode = node.out();
|
||||
assert.equal(outNode.getType(), ASTNode.types.INPUT);
|
||||
assert.equal(
|
||||
outNode.getLocation(),
|
||||
this.blocks.secondBlock.inputList[0].connection,
|
||||
);
|
||||
});
|
||||
test('fromBlockToStack', function () {
|
||||
const node = ASTNode.createBlockNode(this.blocks.statementInput2);
|
||||
const outNode = node.out();
|
||||
assert.equal(outNode.getType(), ASTNode.types.STACK);
|
||||
assert.equal(outNode.getLocation(), this.blocks.statementInput1);
|
||||
});
|
||||
test('fromBlockToInput', function () {
|
||||
const input = this.blocks.statementInput2.inputList[1].connection;
|
||||
const node = ASTNode.createBlockNode(this.blocks.statementInput3);
|
||||
const outNode = node.out();
|
||||
assert.equal(outNode.getType(), ASTNode.types.INPUT);
|
||||
assert.equal(outNode.getLocation(), input);
|
||||
});
|
||||
test('fromTopBlockToStack', function () {
|
||||
const node = ASTNode.createBlockNode(this.blocks.statementInput1);
|
||||
const outNode = node.out();
|
||||
assert.equal(outNode.getType(), ASTNode.types.STACK);
|
||||
assert.equal(outNode.getLocation(), this.blocks.statementInput1);
|
||||
});
|
||||
test('fromBlockToStack_OutputConnection', function () {
|
||||
const node = ASTNode.createBlockNode(this.blocks.fieldWithOutput2);
|
||||
const outNode = node.out();
|
||||
assert.equal(outNode.getType(), ASTNode.types.STACK);
|
||||
assert.equal(outNode.getLocation(), this.blocks.fieldWithOutput2);
|
||||
});
|
||||
test('fromBlockToInput_OutputConnection', function () {
|
||||
const node = ASTNode.createBlockNode(this.blocks.outputNextBlock);
|
||||
const inputConnection = this.blocks.secondBlock.inputList[0].connection;
|
||||
const outNode = node.out();
|
||||
assert.equal(outNode.getType(), ASTNode.types.INPUT);
|
||||
assert.equal(outNode.getLocation(), inputConnection);
|
||||
});
|
||||
});
|
||||
|
||||
suite('createFunctions', function () {
|
||||
test('createFieldNode', function () {
|
||||
const field = this.blocks.statementInput1.inputList[0].fieldRow[0];
|
||||
const node = ASTNode.createFieldNode(field);
|
||||
assert.equal(node.getLocation(), field);
|
||||
assert.equal(node.getType(), ASTNode.types.FIELD);
|
||||
assert.isFalse(node.isConnection());
|
||||
});
|
||||
test('createConnectionNode', function () {
|
||||
const prevConnection = this.blocks.statementInput4.previousConnection;
|
||||
const node = ASTNode.createConnectionNode(prevConnection);
|
||||
assert.equal(node.getLocation(), prevConnection);
|
||||
assert.equal(node.getType(), ASTNode.types.PREVIOUS);
|
||||
assert.isTrue(node.isConnection());
|
||||
});
|
||||
test('createInputNode', function () {
|
||||
const input = this.blocks.statementInput1.inputList[0];
|
||||
const node = ASTNode.createInputNode(input);
|
||||
assert.equal(node.getLocation(), input.connection);
|
||||
assert.equal(node.getType(), ASTNode.types.INPUT);
|
||||
assert.isTrue(node.isConnection());
|
||||
});
|
||||
test('createWorkspaceNode', function () {
|
||||
const coordinate = new Blockly.utils.Coordinate(100, 100);
|
||||
const node = ASTNode.createWorkspaceNode(this.workspace, coordinate);
|
||||
assert.equal(node.getLocation(), this.workspace);
|
||||
assert.equal(node.getType(), ASTNode.types.WORKSPACE);
|
||||
assert.equal(node.getWsCoordinate(), coordinate);
|
||||
assert.isFalse(node.isConnection());
|
||||
});
|
||||
test('createStatementConnectionNode', function () {
|
||||
const nextConnection =
|
||||
this.blocks.statementInput1.inputList[1].connection;
|
||||
const inputConnection =
|
||||
this.blocks.statementInput1.inputList[1].connection;
|
||||
const node = ASTNode.createConnectionNode(nextConnection);
|
||||
assert.equal(node.getLocation(), inputConnection);
|
||||
assert.equal(node.getType(), ASTNode.types.INPUT);
|
||||
assert.isTrue(node.isConnection());
|
||||
});
|
||||
test('createTopNode-previous', function () {
|
||||
const block = this.blocks.statementInput1;
|
||||
const topNode = ASTNode.createTopNode(block);
|
||||
assert.equal(topNode.getLocation(), block.previousConnection);
|
||||
});
|
||||
test('createTopNode-block', function () {
|
||||
const block = this.blocks.noPrevConnection;
|
||||
const topNode = ASTNode.createTopNode(block);
|
||||
assert.equal(topNode.getLocation(), block);
|
||||
});
|
||||
test('createTopNode-output', function () {
|
||||
const block = this.blocks.outputNextBlock;
|
||||
const topNode = ASTNode.createTopNode(block);
|
||||
assert.equal(topNode.getLocation(), block.outputConnection);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -4,7 +4,6 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {ASTNode} from '../../build/src/core/keyboard_nav/ast_node.js';
|
||||
import {assert} from '../../node_modules/chai/chai.js';
|
||||
import {createRenderedBlock} from './test_helpers/block_definitions.js';
|
||||
import {
|
||||
@@ -87,70 +86,63 @@ suite('Cursor', function () {
|
||||
});
|
||||
|
||||
test('Next - From a Previous connection go to the next block', function () {
|
||||
const prevNode = ASTNode.createConnectionNode(
|
||||
this.blocks.A.previousConnection,
|
||||
);
|
||||
const prevNode = this.blocks.A.previousConnection;
|
||||
this.cursor.setCurNode(prevNode);
|
||||
this.cursor.next();
|
||||
const curNode = this.cursor.getCurNode();
|
||||
assert.equal(curNode.getLocation(), this.blocks.A);
|
||||
assert.equal(curNode, this.blocks.A);
|
||||
});
|
||||
test('Next - From a block go to its statement input', function () {
|
||||
const prevNode = ASTNode.createBlockNode(this.blocks.B);
|
||||
const prevNode = this.blocks.B;
|
||||
this.cursor.setCurNode(prevNode);
|
||||
this.cursor.next();
|
||||
const curNode = this.cursor.getCurNode();
|
||||
assert.equal(
|
||||
curNode.getLocation(),
|
||||
this.blocks.B.getInput('NAME4').connection,
|
||||
);
|
||||
assert.equal(curNode, this.blocks.B.getInput('NAME4').connection);
|
||||
});
|
||||
|
||||
test('In - From attached input connection', function () {
|
||||
const fieldBlock = this.blocks.E;
|
||||
const inputConnectionNode = ASTNode.createConnectionNode(
|
||||
this.blocks.A.inputList[0].connection,
|
||||
);
|
||||
const inputConnectionNode = this.blocks.A.inputList[0].connection;
|
||||
this.cursor.setCurNode(inputConnectionNode);
|
||||
this.cursor.in();
|
||||
const curNode = this.cursor.getCurNode();
|
||||
assert.equal(curNode.getLocation(), fieldBlock);
|
||||
assert.equal(curNode, fieldBlock);
|
||||
});
|
||||
|
||||
test('Prev - From previous connection does not skip over next connection', function () {
|
||||
const prevConnection = this.blocks.B.previousConnection;
|
||||
const prevConnectionNode = ASTNode.createConnectionNode(prevConnection);
|
||||
const prevConnectionNode = prevConnection;
|
||||
this.cursor.setCurNode(prevConnectionNode);
|
||||
this.cursor.prev();
|
||||
const curNode = this.cursor.getCurNode();
|
||||
assert.equal(curNode.getLocation(), this.blocks.A.nextConnection);
|
||||
assert.equal(curNode, this.blocks.A.nextConnection);
|
||||
});
|
||||
|
||||
test('Prev - From first connection loop to last next connection', function () {
|
||||
const prevConnection = this.blocks.A.previousConnection;
|
||||
const prevConnectionNode = ASTNode.createConnectionNode(prevConnection);
|
||||
const prevConnectionNode = prevConnection;
|
||||
this.cursor.setCurNode(prevConnectionNode);
|
||||
this.cursor.prev();
|
||||
const curNode = this.cursor.getCurNode();
|
||||
assert.equal(curNode.getLocation(), this.blocks.D.nextConnection);
|
||||
assert.equal(curNode, this.blocks.D.nextConnection);
|
||||
});
|
||||
|
||||
test('Out - From field does not skip over block node', function () {
|
||||
const field = this.blocks.E.inputList[0].fieldRow[0];
|
||||
const fieldNode = ASTNode.createFieldNode(field);
|
||||
const fieldNode = field;
|
||||
this.cursor.setCurNode(fieldNode);
|
||||
this.cursor.out();
|
||||
const curNode = this.cursor.getCurNode();
|
||||
assert.equal(curNode.getLocation(), this.blocks.E);
|
||||
assert.equal(curNode, this.blocks.E);
|
||||
});
|
||||
|
||||
test('Out - From first connection loop to last next connection', function () {
|
||||
const prevConnection = this.blocks.A.previousConnection;
|
||||
const prevConnectionNode = ASTNode.createConnectionNode(prevConnection);
|
||||
const prevConnectionNode = prevConnection;
|
||||
this.cursor.setCurNode(prevConnectionNode);
|
||||
this.cursor.out();
|
||||
const curNode = this.cursor.getCurNode();
|
||||
assert.equal(curNode.getLocation(), this.blocks.D.nextConnection);
|
||||
assert.equal(curNode, this.blocks.D.nextConnection);
|
||||
});
|
||||
});
|
||||
suite('Searching', function () {
|
||||
@@ -216,11 +208,11 @@ suite('Cursor', function () {
|
||||
});
|
||||
test('getFirstNode', function () {
|
||||
const node = this.cursor.getFirstNode();
|
||||
assert.equal(node.getLocation(), this.blockA);
|
||||
assert.equal(node, this.blockA);
|
||||
});
|
||||
test('getLastNode', function () {
|
||||
const node = this.cursor.getLastNode();
|
||||
assert.equal(node.getLocation(), this.blockA);
|
||||
assert.equal(node, this.blockA);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -233,11 +225,11 @@ suite('Cursor', function () {
|
||||
});
|
||||
test('getFirstNode', function () {
|
||||
const node = this.cursor.getFirstNode();
|
||||
assert.equal(node.getLocation(), this.blockA.previousConnection);
|
||||
assert.equal(node, this.blockA.previousConnection);
|
||||
});
|
||||
test('getLastNode', function () {
|
||||
const node = this.cursor.getLastNode();
|
||||
assert.equal(node.getLocation(), this.blockA.nextConnection);
|
||||
assert.equal(node, this.blockA.nextConnection);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -250,11 +242,11 @@ suite('Cursor', function () {
|
||||
});
|
||||
test('getFirstNode', function () {
|
||||
const node = this.cursor.getFirstNode();
|
||||
assert.equal(node.getLocation(), this.blockA.outputConnection);
|
||||
assert.equal(node, this.blockA.outputConnection);
|
||||
});
|
||||
test('getLastNode', function () {
|
||||
const node = this.cursor.getLastNode();
|
||||
assert.equal(node.getLocation(), this.blockA.inputList[0].connection);
|
||||
assert.equal(node, this.blockA.inputList[0].connection);
|
||||
});
|
||||
});
|
||||
suite('one c-hat block', function () {
|
||||
@@ -266,11 +258,11 @@ suite('Cursor', function () {
|
||||
});
|
||||
test('getFirstNode', function () {
|
||||
const node = this.cursor.getFirstNode();
|
||||
assert.equal(node.getLocation(), this.blockA);
|
||||
assert.equal(node, this.blockA);
|
||||
});
|
||||
test('getLastNode', function () {
|
||||
const node = this.cursor.getLastNode();
|
||||
assert.equal(node.getLocation(), this.blockA.inputList[0].connection);
|
||||
assert.equal(node, this.blockA.inputList[0].connection);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -303,12 +295,12 @@ suite('Cursor', function () {
|
||||
test('getFirstNode', function () {
|
||||
const node = this.cursor.getFirstNode();
|
||||
const blockA = this.workspace.getBlockById('A');
|
||||
assert.equal(node.getLocation(), blockA.previousConnection);
|
||||
assert.equal(node, blockA.previousConnection);
|
||||
});
|
||||
test('getLastNode', function () {
|
||||
const node = this.cursor.getLastNode();
|
||||
const blockB = this.workspace.getBlockById('B');
|
||||
assert.equal(node.getLocation(), blockB.nextConnection);
|
||||
assert.equal(node, blockB.nextConnection);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -343,14 +335,15 @@ suite('Cursor', function () {
|
||||
test('getFirstNode', function () {
|
||||
const node = this.cursor.getFirstNode();
|
||||
const blockA = this.workspace.getBlockById('A');
|
||||
assert.equal(node.getLocation(), blockA.outputConnection);
|
||||
assert.equal(node, blockA.outputConnection);
|
||||
});
|
||||
test('getLastNode', function () {
|
||||
const node = this.cursor.getLastNode();
|
||||
const blockB = this.workspace.getBlockById('B');
|
||||
assert.equal(node.getLocation(), blockB.inputList[0].connection);
|
||||
assert.equal(node, blockB.inputList[0].connection);
|
||||
});
|
||||
});
|
||||
|
||||
suite('two stacks', function () {
|
||||
setup(function () {
|
||||
const state = {
|
||||
@@ -391,14 +384,14 @@ suite('Cursor', function () {
|
||||
});
|
||||
test('getFirstNode', function () {
|
||||
const node = this.cursor.getFirstNode();
|
||||
const location = node.getLocation();
|
||||
const location = node;
|
||||
const previousConnection =
|
||||
this.workspace.getBlockById('A').previousConnection;
|
||||
assert.equal(location, previousConnection);
|
||||
});
|
||||
test('getLastNode', function () {
|
||||
const node = this.cursor.getLastNode();
|
||||
const location = node.getLocation();
|
||||
const location = node;
|
||||
const nextConnection = this.workspace.getBlockById('D').nextConnection;
|
||||
assert.equal(location, nextConnection);
|
||||
});
|
||||
@@ -447,7 +440,7 @@ suite('Cursor', function () {
|
||||
this.neverValid = () => false;
|
||||
this.alwaysValid = () => true;
|
||||
this.isConnection = (node) => {
|
||||
return node && node.isConnection();
|
||||
return node && node instanceof Blockly.RenderedConnection;
|
||||
};
|
||||
});
|
||||
teardown(function () {
|
||||
@@ -489,9 +482,7 @@ suite('Cursor', function () {
|
||||
this.workspace.clear();
|
||||
});
|
||||
test('Never valid - start at top', function () {
|
||||
const startNode = ASTNode.createConnectionNode(
|
||||
this.blockA.previousConnection,
|
||||
);
|
||||
const startNode = this.blockA.previousConnection;
|
||||
const nextNode = this.cursor.getNextNode(
|
||||
startNode,
|
||||
this.neverValid,
|
||||
@@ -500,7 +491,7 @@ suite('Cursor', function () {
|
||||
assert.isNull(nextNode);
|
||||
});
|
||||
test('Never valid - start in middle', function () {
|
||||
const startNode = ASTNode.createBlockNode(this.blockB);
|
||||
const startNode = this.blockB;
|
||||
const nextNode = this.cursor.getNextNode(
|
||||
startNode,
|
||||
this.neverValid,
|
||||
@@ -509,9 +500,7 @@ suite('Cursor', function () {
|
||||
assert.isNull(nextNode);
|
||||
});
|
||||
test('Never valid - start at end', function () {
|
||||
const startNode = ASTNode.createConnectionNode(
|
||||
this.blockC.nextConnection,
|
||||
);
|
||||
const startNode = this.blockC.nextConnection;
|
||||
const nextNode = this.cursor.getNextNode(
|
||||
startNode,
|
||||
this.neverValid,
|
||||
@@ -521,29 +510,25 @@ suite('Cursor', function () {
|
||||
});
|
||||
|
||||
test('Always valid - start at top', function () {
|
||||
const startNode = ASTNode.createConnectionNode(
|
||||
this.blockA.previousConnection,
|
||||
);
|
||||
const startNode = this.blockA.previousConnection;
|
||||
const nextNode = this.cursor.getNextNode(
|
||||
startNode,
|
||||
this.alwaysValid,
|
||||
false,
|
||||
);
|
||||
assert.equal(nextNode.getLocation(), this.blockA);
|
||||
assert.equal(nextNode, this.blockA);
|
||||
});
|
||||
test('Always valid - start in middle', function () {
|
||||
const startNode = ASTNode.createBlockNode(this.blockB);
|
||||
const startNode = this.blockB;
|
||||
const nextNode = this.cursor.getNextNode(
|
||||
startNode,
|
||||
this.alwaysValid,
|
||||
false,
|
||||
);
|
||||
assert.equal(nextNode.getLocation(), this.blockB.getField('FIELD'));
|
||||
assert.equal(nextNode, this.blockB.getField('FIELD'));
|
||||
});
|
||||
test('Always valid - start at end', function () {
|
||||
const startNode = ASTNode.createConnectionNode(
|
||||
this.blockC.nextConnection,
|
||||
);
|
||||
const startNode = this.blockC.nextConnection;
|
||||
const nextNode = this.cursor.getNextNode(
|
||||
startNode,
|
||||
this.alwaysValid,
|
||||
@@ -553,29 +538,25 @@ suite('Cursor', function () {
|
||||
});
|
||||
|
||||
test('Valid if connection - start at top', function () {
|
||||
const startNode = ASTNode.createConnectionNode(
|
||||
this.blockA.previousConnection,
|
||||
);
|
||||
const startNode = this.blockA.previousConnection;
|
||||
const nextNode = this.cursor.getNextNode(
|
||||
startNode,
|
||||
this.isConnection,
|
||||
false,
|
||||
);
|
||||
assert.equal(nextNode.getLocation(), this.blockA.nextConnection);
|
||||
assert.equal(nextNode, this.blockA.nextConnection);
|
||||
});
|
||||
test('Valid if connection - start in middle', function () {
|
||||
const startNode = ASTNode.createBlockNode(this.blockB);
|
||||
const startNode = this.blockB;
|
||||
const nextNode = this.cursor.getNextNode(
|
||||
startNode,
|
||||
this.isConnection,
|
||||
false,
|
||||
);
|
||||
assert.equal(nextNode.getLocation(), this.blockB.nextConnection);
|
||||
assert.equal(nextNode, this.blockB.nextConnection);
|
||||
});
|
||||
test('Valid if connection - start at end', function () {
|
||||
const startNode = ASTNode.createConnectionNode(
|
||||
this.blockC.nextConnection,
|
||||
);
|
||||
const startNode = this.blockC.nextConnection;
|
||||
const nextNode = this.cursor.getNextNode(
|
||||
startNode,
|
||||
this.isConnection,
|
||||
@@ -584,9 +565,7 @@ suite('Cursor', function () {
|
||||
assert.isNull(nextNode);
|
||||
});
|
||||
test('Never valid - start at end - with loopback', function () {
|
||||
const startNode = ASTNode.createConnectionNode(
|
||||
this.blockC.nextConnection,
|
||||
);
|
||||
const startNode = this.blockC.nextConnection;
|
||||
const nextNode = this.cursor.getNextNode(
|
||||
startNode,
|
||||
this.neverValid,
|
||||
@@ -595,27 +574,23 @@ suite('Cursor', function () {
|
||||
assert.isNull(nextNode);
|
||||
});
|
||||
test('Always valid - start at end - with loopback', function () {
|
||||
const startNode = ASTNode.createConnectionNode(
|
||||
this.blockC.nextConnection,
|
||||
);
|
||||
const startNode = this.blockC.nextConnection;
|
||||
const nextNode = this.cursor.getNextNode(
|
||||
startNode,
|
||||
this.alwaysValid,
|
||||
true,
|
||||
);
|
||||
assert.equal(nextNode.getLocation(), this.blockA.previousConnection);
|
||||
assert.equal(nextNode, this.blockA.previousConnection);
|
||||
});
|
||||
|
||||
test('Valid if connection - start at end - with loopback', function () {
|
||||
const startNode = ASTNode.createConnectionNode(
|
||||
this.blockC.nextConnection,
|
||||
);
|
||||
const startNode = this.blockC.nextConnection;
|
||||
const nextNode = this.cursor.getNextNode(
|
||||
startNode,
|
||||
this.isConnection,
|
||||
true,
|
||||
);
|
||||
assert.equal(nextNode.getLocation(), this.blockA.previousConnection);
|
||||
assert.equal(nextNode, this.blockA.previousConnection);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -663,7 +638,7 @@ suite('Cursor', function () {
|
||||
this.neverValid = () => false;
|
||||
this.alwaysValid = () => true;
|
||||
this.isConnection = (node) => {
|
||||
return node && node.isConnection();
|
||||
return node && node instanceof Blockly.RenderedConnection;
|
||||
};
|
||||
});
|
||||
teardown(function () {
|
||||
@@ -705,9 +680,7 @@ suite('Cursor', function () {
|
||||
this.workspace.clear();
|
||||
});
|
||||
test('Never valid - start at top', function () {
|
||||
const startNode = ASTNode.createConnectionNode(
|
||||
this.blockA.previousConnection,
|
||||
);
|
||||
const startNode = this.blockA.previousConnection;
|
||||
const previousNode = this.cursor.getPreviousNode(
|
||||
startNode,
|
||||
this.neverValid,
|
||||
@@ -716,7 +689,7 @@ suite('Cursor', function () {
|
||||
assert.isNull(previousNode);
|
||||
});
|
||||
test('Never valid - start in middle', function () {
|
||||
const startNode = ASTNode.createBlockNode(this.blockB);
|
||||
const startNode = this.blockB;
|
||||
const previousNode = this.cursor.getPreviousNode(
|
||||
startNode,
|
||||
this.neverValid,
|
||||
@@ -725,9 +698,7 @@ suite('Cursor', function () {
|
||||
assert.isNull(previousNode);
|
||||
});
|
||||
test('Never valid - start at end', function () {
|
||||
const startNode = ASTNode.createConnectionNode(
|
||||
this.blockC.nextConnection,
|
||||
);
|
||||
const startNode = this.blockC.nextConnection;
|
||||
const previousNode = this.cursor.getPreviousNode(
|
||||
startNode,
|
||||
this.neverValid,
|
||||
@@ -737,44 +708,35 @@ suite('Cursor', function () {
|
||||
});
|
||||
|
||||
test('Always valid - start at top', function () {
|
||||
const startNode = ASTNode.createConnectionNode(
|
||||
this.blockA.previousConnection,
|
||||
);
|
||||
const startNode = this.blockA.previousConnection;
|
||||
const previousNode = this.cursor.getPreviousNode(
|
||||
startNode,
|
||||
this.alwaysValid,
|
||||
false,
|
||||
);
|
||||
assert.isNotNull(previousNode);
|
||||
assert.isNull(previousNode);
|
||||
});
|
||||
test('Always valid - start in middle', function () {
|
||||
const startNode = ASTNode.createBlockNode(this.blockB);
|
||||
const startNode = this.blockB;
|
||||
const previousNode = this.cursor.getPreviousNode(
|
||||
startNode,
|
||||
this.alwaysValid,
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
previousNode.getLocation(),
|
||||
this.blockB.previousConnection,
|
||||
);
|
||||
assert.equal(previousNode, this.blockB.previousConnection);
|
||||
});
|
||||
test('Always valid - start at end', function () {
|
||||
const startNode = ASTNode.createConnectionNode(
|
||||
this.blockC.nextConnection,
|
||||
);
|
||||
const startNode = this.blockC.nextConnection;
|
||||
const previousNode = this.cursor.getPreviousNode(
|
||||
startNode,
|
||||
this.alwaysValid,
|
||||
false,
|
||||
);
|
||||
assert.equal(previousNode.getLocation(), this.blockC.getField('FIELD'));
|
||||
assert.equal(previousNode, this.blockC.getField('FIELD'));
|
||||
});
|
||||
|
||||
test('Valid if connection - start at top', function () {
|
||||
const startNode = ASTNode.createConnectionNode(
|
||||
this.blockA.previousConnection,
|
||||
);
|
||||
const startNode = this.blockA.previousConnection;
|
||||
const previousNode = this.cursor.getPreviousNode(
|
||||
startNode,
|
||||
this.isConnection,
|
||||
@@ -783,35 +745,25 @@ suite('Cursor', function () {
|
||||
assert.isNull(previousNode);
|
||||
});
|
||||
test('Valid if connection - start in middle', function () {
|
||||
const startNode = ASTNode.createBlockNode(this.blockB);
|
||||
const startNode = this.blockB;
|
||||
const previousNode = this.cursor.getPreviousNode(
|
||||
startNode,
|
||||
this.isConnection,
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
previousNode.getLocation(),
|
||||
this.blockB.previousConnection,
|
||||
);
|
||||
assert.equal(previousNode, this.blockB.previousConnection);
|
||||
});
|
||||
test('Valid if connection - start at end', function () {
|
||||
const startNode = ASTNode.createConnectionNode(
|
||||
this.blockC.nextConnection,
|
||||
);
|
||||
const startNode = this.blockC.nextConnection;
|
||||
const previousNode = this.cursor.getPreviousNode(
|
||||
startNode,
|
||||
this.isConnection,
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
previousNode.getLocation(),
|
||||
this.blockC.previousConnection,
|
||||
);
|
||||
assert.equal(previousNode, this.blockC.previousConnection);
|
||||
});
|
||||
test('Never valid - start at top - with loopback', function () {
|
||||
const startNode = ASTNode.createConnectionNode(
|
||||
this.blockA.previousConnection,
|
||||
);
|
||||
const startNode = this.blockA.previousConnection;
|
||||
const previousNode = this.cursor.getPreviousNode(
|
||||
startNode,
|
||||
this.neverValid,
|
||||
@@ -820,27 +772,22 @@ suite('Cursor', function () {
|
||||
assert.isNull(previousNode);
|
||||
});
|
||||
test('Always valid - start at top - with loopback', function () {
|
||||
const startNode = ASTNode.createConnectionNode(
|
||||
this.blockA.previousConnection,
|
||||
);
|
||||
const startNode = this.blockA.previousConnection;
|
||||
const previousNode = this.cursor.getPreviousNode(
|
||||
startNode,
|
||||
this.alwaysValid,
|
||||
true,
|
||||
);
|
||||
// Previous node will be a stack node in this case.
|
||||
assert.equal(previousNode.getLocation(), this.blockA);
|
||||
assert.equal(previousNode, this.blockC.nextConnection);
|
||||
});
|
||||
test('Valid if connection - start at top - with loopback', function () {
|
||||
const startNode = ASTNode.createConnectionNode(
|
||||
this.blockA.previousConnection,
|
||||
);
|
||||
const startNode = this.blockA.previousConnection;
|
||||
const previousNode = this.cursor.getPreviousNode(
|
||||
startNode,
|
||||
this.isConnection,
|
||||
true,
|
||||
);
|
||||
assert.equal(previousNode.getLocation(), this.blockC.nextConnection);
|
||||
assert.equal(previousNode, this.blockC.nextConnection);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -180,7 +180,6 @@
|
||||
import {javascriptGenerator} from '../../build/javascript.loader.mjs';
|
||||
|
||||
// Import tests.
|
||||
import './astnode_test.js';
|
||||
import './block_json_test.js';
|
||||
import './block_test.js';
|
||||
import './clipboard_test.js';
|
||||
@@ -249,6 +248,7 @@
|
||||
import './metrics_test.js';
|
||||
import './mutator_test.js';
|
||||
import './names_test.js';
|
||||
import './navigation_test.js';
|
||||
// TODO: Remove these tests.
|
||||
import './old_workspace_comment_test.js';
|
||||
import './procedure_map_test.js';
|
||||
|
||||
567
tests/mocha/navigation_test.js
Normal file
567
tests/mocha/navigation_test.js
Normal file
@@ -0,0 +1,567 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {assert} from '../../node_modules/chai/chai.js';
|
||||
import {
|
||||
sharedTestSetup,
|
||||
sharedTestTeardown,
|
||||
workspaceTeardown,
|
||||
} from './test_helpers/setup_teardown.js';
|
||||
|
||||
suite('Navigation', function () {
|
||||
setup(function () {
|
||||
sharedTestSetup.call(this);
|
||||
Blockly.defineBlocksWithJsonArray([
|
||||
{
|
||||
'type': 'input_statement',
|
||||
'message0': '%1 %2 %3 %4',
|
||||
'args0': [
|
||||
{
|
||||
'type': 'field_input',
|
||||
'name': 'NAME',
|
||||
'text': 'default',
|
||||
},
|
||||
{
|
||||
'type': 'field_input',
|
||||
'name': 'NAME',
|
||||
'text': 'default',
|
||||
},
|
||||
{
|
||||
'type': 'input_value',
|
||||
'name': 'NAME',
|
||||
},
|
||||
{
|
||||
'type': 'input_statement',
|
||||
'name': 'NAME',
|
||||
},
|
||||
],
|
||||
'previousStatement': null,
|
||||
'nextStatement': null,
|
||||
'colour': 230,
|
||||
'tooltip': '',
|
||||
'helpUrl': '',
|
||||
},
|
||||
{
|
||||
'type': 'value_input',
|
||||
'message0': '%1',
|
||||
'args0': [
|
||||
{
|
||||
'type': 'input_value',
|
||||
'name': 'NAME',
|
||||
},
|
||||
],
|
||||
'colour': 230,
|
||||
'tooltip': '',
|
||||
'helpUrl': '',
|
||||
},
|
||||
{
|
||||
'type': 'field_input',
|
||||
'message0': '%1',
|
||||
'args0': [
|
||||
{
|
||||
'type': 'field_input',
|
||||
'name': 'NAME',
|
||||
'text': 'default',
|
||||
},
|
||||
],
|
||||
'output': null,
|
||||
'colour': 230,
|
||||
'tooltip': '',
|
||||
'helpUrl': '',
|
||||
},
|
||||
]);
|
||||
this.workspace = Blockly.inject('blocklyDiv', {});
|
||||
this.navigator = this.workspace.getNavigator();
|
||||
const statementInput1 = this.workspace.newBlock('input_statement');
|
||||
const statementInput2 = this.workspace.newBlock('input_statement');
|
||||
const statementInput3 = this.workspace.newBlock('input_statement');
|
||||
const statementInput4 = this.workspace.newBlock('input_statement');
|
||||
const fieldWithOutput = this.workspace.newBlock('field_input');
|
||||
const valueInput = this.workspace.newBlock('value_input');
|
||||
|
||||
statementInput1.nextConnection.connect(statementInput2.previousConnection);
|
||||
statementInput1.inputList[0].connection.connect(
|
||||
fieldWithOutput.outputConnection,
|
||||
);
|
||||
statementInput2.inputList[1].connection.connect(
|
||||
statementInput3.previousConnection,
|
||||
);
|
||||
|
||||
this.blocks = {
|
||||
statementInput1: statementInput1,
|
||||
statementInput2: statementInput2,
|
||||
statementInput3: statementInput3,
|
||||
statementInput4: statementInput4,
|
||||
fieldWithOutput: fieldWithOutput,
|
||||
valueInput: valueInput,
|
||||
};
|
||||
});
|
||||
teardown(function () {
|
||||
sharedTestTeardown.call(this);
|
||||
});
|
||||
|
||||
suite('NavigationFunctions', function () {
|
||||
setup(function () {
|
||||
Blockly.defineBlocksWithJsonArray([
|
||||
{
|
||||
'type': 'top_connection',
|
||||
'message0': '',
|
||||
'previousStatement': null,
|
||||
'colour': 230,
|
||||
'tooltip': '',
|
||||
'helpUrl': '',
|
||||
},
|
||||
{
|
||||
'type': 'start_block',
|
||||
'message0': '',
|
||||
'nextStatement': null,
|
||||
'colour': 230,
|
||||
'tooltip': '',
|
||||
'helpUrl': '',
|
||||
},
|
||||
{
|
||||
'type': 'fields_and_input',
|
||||
'message0': '%1 hi %2 %3 %4',
|
||||
'args0': [
|
||||
{
|
||||
'type': 'field_input',
|
||||
'name': 'NAME',
|
||||
'text': 'default',
|
||||
},
|
||||
{
|
||||
'type': 'input_dummy',
|
||||
},
|
||||
{
|
||||
'type': 'field_input',
|
||||
'name': 'NAME',
|
||||
'text': 'default',
|
||||
},
|
||||
{
|
||||
'type': 'input_value',
|
||||
'name': 'NAME',
|
||||
},
|
||||
],
|
||||
'previousStatement': null,
|
||||
'nextStatement': null,
|
||||
'colour': 230,
|
||||
'tooltip': '',
|
||||
'helpUrl': '',
|
||||
},
|
||||
{
|
||||
'type': 'two_fields',
|
||||
'message0': '%1 hi',
|
||||
'args0': [
|
||||
{
|
||||
'type': 'field_input',
|
||||
'name': 'NAME',
|
||||
'text': 'default',
|
||||
},
|
||||
],
|
||||
'colour': 230,
|
||||
'tooltip': '',
|
||||
'helpUrl': '',
|
||||
},
|
||||
{
|
||||
'type': 'fields_and_input2',
|
||||
'message0': '%1 %2 %3 hi %4 bye',
|
||||
'args0': [
|
||||
{
|
||||
'type': 'input_value',
|
||||
'name': 'NAME',
|
||||
},
|
||||
{
|
||||
'type': 'field_input',
|
||||
'name': 'NAME',
|
||||
'text': 'default',
|
||||
},
|
||||
{
|
||||
'type': 'input_value',
|
||||
'name': 'NAME',
|
||||
},
|
||||
{
|
||||
'type': 'input_statement',
|
||||
'name': 'NAME',
|
||||
},
|
||||
],
|
||||
'colour': 230,
|
||||
'tooltip': '',
|
||||
'helpUrl': '',
|
||||
},
|
||||
{
|
||||
'type': 'dummy_input',
|
||||
'message0': 'Hello',
|
||||
'colour': 230,
|
||||
'tooltip': '',
|
||||
'helpUrl': '',
|
||||
},
|
||||
{
|
||||
'type': 'dummy_inputValue',
|
||||
'message0': 'Hello %1 %2',
|
||||
'args0': [
|
||||
{
|
||||
'type': 'input_dummy',
|
||||
},
|
||||
{
|
||||
'type': 'input_value',
|
||||
'name': 'NAME',
|
||||
},
|
||||
],
|
||||
'colour': 230,
|
||||
'tooltip': '',
|
||||
'helpUrl': '',
|
||||
},
|
||||
{
|
||||
'type': 'output_next',
|
||||
'message0': '',
|
||||
'output': null,
|
||||
'colour': 230,
|
||||
'tooltip': '',
|
||||
'helpUrl': '',
|
||||
'nextStatement': null,
|
||||
},
|
||||
]);
|
||||
const noNextConnection = this.workspace.newBlock('top_connection');
|
||||
const fieldAndInputs = this.workspace.newBlock('fields_and_input');
|
||||
const twoFields = this.workspace.newBlock('two_fields');
|
||||
const fieldAndInputs2 = this.workspace.newBlock('fields_and_input2');
|
||||
const noPrevConnection = this.workspace.newBlock('start_block');
|
||||
this.blocks.noNextConnection = noNextConnection;
|
||||
this.blocks.fieldAndInputs = fieldAndInputs;
|
||||
this.blocks.twoFields = twoFields;
|
||||
this.blocks.fieldAndInputs2 = fieldAndInputs2;
|
||||
this.blocks.noPrevConnection = noPrevConnection;
|
||||
|
||||
const dummyInput = this.workspace.newBlock('dummy_input');
|
||||
const dummyInputValue = this.workspace.newBlock('dummy_inputValue');
|
||||
const fieldWithOutput2 = this.workspace.newBlock('field_input');
|
||||
this.blocks.dummyInput = dummyInput;
|
||||
this.blocks.dummyInputValue = dummyInputValue;
|
||||
this.blocks.fieldWithOutput2 = fieldWithOutput2;
|
||||
|
||||
const secondBlock = this.workspace.newBlock('input_statement');
|
||||
const outputNextBlock = this.workspace.newBlock('output_next');
|
||||
this.blocks.secondBlock = secondBlock;
|
||||
this.blocks.outputNextBlock = outputNextBlock;
|
||||
});
|
||||
suite('Next', function () {
|
||||
setup(function () {
|
||||
this.singleBlockWorkspace = new Blockly.Workspace();
|
||||
const singleBlock = this.singleBlockWorkspace.newBlock('two_fields');
|
||||
this.blocks.singleBlock = singleBlock;
|
||||
});
|
||||
teardown(function () {
|
||||
workspaceTeardown.call(this, this.singleBlockWorkspace);
|
||||
});
|
||||
|
||||
test('fromPreviousToBlock', function () {
|
||||
const prevConnection = this.blocks.statementInput1.previousConnection;
|
||||
const nextNode = this.navigator.getNextSibling(prevConnection);
|
||||
assert.equal(nextNode, this.blocks.statementInput1);
|
||||
});
|
||||
test('fromBlockToNext', function () {
|
||||
const nextConnection = this.blocks.statementInput1.nextConnection;
|
||||
const nextNode = this.navigator.getNextSibling(
|
||||
this.blocks.statementInput1,
|
||||
);
|
||||
assert.equal(nextNode, nextConnection);
|
||||
});
|
||||
test('fromNextToPrevious', function () {
|
||||
const nextConnection = this.blocks.statementInput1.nextConnection;
|
||||
const prevConnection = this.blocks.statementInput2.previousConnection;
|
||||
const nextNode = this.navigator.getNextSibling(nextConnection);
|
||||
assert.equal(nextNode, prevConnection);
|
||||
});
|
||||
test('fromInputToInput', function () {
|
||||
const input = this.blocks.statementInput1.inputList[0];
|
||||
const inputConnection =
|
||||
this.blocks.statementInput1.inputList[1].connection;
|
||||
const nextNode = this.navigator.getNextSibling(input.connection);
|
||||
assert.equal(nextNode, inputConnection);
|
||||
});
|
||||
test('fromInputToStatementInput', function () {
|
||||
const input = this.blocks.fieldAndInputs2.inputList[1];
|
||||
const inputConnection =
|
||||
this.blocks.fieldAndInputs2.inputList[2].connection;
|
||||
const nextNode = this.navigator.getNextSibling(input.connection);
|
||||
assert.equal(nextNode, inputConnection);
|
||||
});
|
||||
test('fromInputToField', function () {
|
||||
const input = this.blocks.fieldAndInputs2.inputList[0];
|
||||
const field = this.blocks.fieldAndInputs2.inputList[1].fieldRow[0];
|
||||
const nextNode = this.navigator.getNextSibling(input.connection);
|
||||
assert.equal(nextNode, field);
|
||||
});
|
||||
test('fromInputToNull', function () {
|
||||
const input = this.blocks.fieldAndInputs2.inputList[2];
|
||||
const nextNode = this.navigator.getNextSibling(input.connection);
|
||||
assert.isNull(nextNode);
|
||||
});
|
||||
test('fromOutputToBlock', function () {
|
||||
const output = this.blocks.fieldWithOutput.outputConnection;
|
||||
const nextNode = this.navigator.getNextSibling(output);
|
||||
assert.equal(nextNode, this.blocks.fieldWithOutput);
|
||||
});
|
||||
test('fromFieldToInput', function () {
|
||||
const field = this.blocks.statementInput1.inputList[0].fieldRow[1];
|
||||
const inputConnection =
|
||||
this.blocks.statementInput1.inputList[0].connection;
|
||||
const nextNode = this.navigator.getNextSibling(field);
|
||||
assert.equal(nextNode, inputConnection);
|
||||
});
|
||||
test('fromFieldToField', function () {
|
||||
const field = this.blocks.fieldAndInputs.inputList[0].fieldRow[0];
|
||||
const field2 = this.blocks.fieldAndInputs.inputList[1].fieldRow[0];
|
||||
const nextNode = this.navigator.getNextSibling(field);
|
||||
assert.equal(nextNode, field2);
|
||||
});
|
||||
test('fromFieldToNull', function () {
|
||||
const field = this.blocks.twoFields.inputList[0].fieldRow[0];
|
||||
const nextNode = this.navigator.getNextSibling(field);
|
||||
assert.isNull(nextNode);
|
||||
});
|
||||
});
|
||||
|
||||
suite('Previous', function () {
|
||||
test('fromPreviousToNext', function () {
|
||||
const prevConnection = this.blocks.statementInput2.previousConnection;
|
||||
const prevNode = this.navigator.getPreviousSibling(prevConnection);
|
||||
const nextConnection = this.blocks.statementInput1.nextConnection;
|
||||
assert.equal(prevNode, nextConnection);
|
||||
});
|
||||
test('fromPreviousToInput', function () {
|
||||
const prevConnection = this.blocks.statementInput3.previousConnection;
|
||||
const prevNode = this.navigator.getPreviousSibling(prevConnection);
|
||||
assert.isNull(prevNode);
|
||||
});
|
||||
test('fromBlockToPrevious', function () {
|
||||
const prevNode = this.navigator.getPreviousSibling(
|
||||
this.blocks.statementInput1,
|
||||
);
|
||||
const prevConnection = this.blocks.statementInput1.previousConnection;
|
||||
assert.equal(prevNode, prevConnection);
|
||||
});
|
||||
test('fromBlockToOutput', function () {
|
||||
const prevNode = this.navigator.getPreviousSibling(
|
||||
this.blocks.fieldWithOutput,
|
||||
);
|
||||
const outputConnection = this.blocks.fieldWithOutput.outputConnection;
|
||||
assert.equal(prevNode, outputConnection);
|
||||
});
|
||||
test('fromNextToBlock', function () {
|
||||
const nextConnection = this.blocks.statementInput1.nextConnection;
|
||||
const prevNode = this.navigator.getPreviousSibling(nextConnection);
|
||||
assert.equal(prevNode, this.blocks.statementInput1);
|
||||
});
|
||||
test('fromInputToField', function () {
|
||||
const input = this.blocks.statementInput1.inputList[0];
|
||||
const prevNode = this.navigator.getPreviousSibling(input.connection);
|
||||
assert.equal(prevNode, input.fieldRow[1]);
|
||||
});
|
||||
test('fromInputToNull', function () {
|
||||
const input = this.blocks.fieldAndInputs2.inputList[0];
|
||||
const prevNode = this.navigator.getPreviousSibling(input.connection);
|
||||
assert.isNull(prevNode);
|
||||
});
|
||||
test('fromInputToInput', function () {
|
||||
const input = this.blocks.fieldAndInputs2.inputList[2];
|
||||
const inputConnection =
|
||||
this.blocks.fieldAndInputs2.inputList[1].connection;
|
||||
const prevNode = this.navigator.getPreviousSibling(input.connection);
|
||||
assert.equal(prevNode, inputConnection);
|
||||
});
|
||||
test('fromOutputToNull', function () {
|
||||
const output = this.blocks.fieldWithOutput.outputConnection;
|
||||
const prevNode = this.navigator.getPreviousSibling(output);
|
||||
assert.isNull(prevNode);
|
||||
});
|
||||
test('fromFieldToNull', function () {
|
||||
const field = this.blocks.statementInput1.inputList[0].fieldRow[0];
|
||||
const prevNode = this.navigator.getPreviousSibling(field);
|
||||
assert.isNull(prevNode);
|
||||
});
|
||||
test('fromFieldToInput', function () {
|
||||
const field = this.blocks.fieldAndInputs2.inputList[1].fieldRow[0];
|
||||
const inputConnection =
|
||||
this.blocks.fieldAndInputs2.inputList[0].connection;
|
||||
const prevNode = this.navigator.getPreviousSibling(field);
|
||||
assert.equal(prevNode, inputConnection);
|
||||
});
|
||||
test('fromFieldToField', function () {
|
||||
const field = this.blocks.fieldAndInputs.inputList[1].fieldRow[0];
|
||||
const field2 = this.blocks.fieldAndInputs.inputList[0].fieldRow[0];
|
||||
const prevNode = this.navigator.getPreviousSibling(field);
|
||||
assert.equal(prevNode, field2);
|
||||
});
|
||||
});
|
||||
|
||||
suite('In', function () {
|
||||
setup(function () {
|
||||
this.emptyWorkspace = Blockly.inject(document.createElement('div'), {});
|
||||
});
|
||||
teardown(function () {
|
||||
workspaceTeardown.call(this, this.emptyWorkspace);
|
||||
});
|
||||
|
||||
test('fromInputToOutput', function () {
|
||||
const input = this.blocks.statementInput1.inputList[0];
|
||||
const inNode = this.navigator.getFirstChild(input.connection);
|
||||
const outputConnection = this.blocks.fieldWithOutput.outputConnection;
|
||||
assert.equal(inNode, outputConnection);
|
||||
});
|
||||
test('fromInputToNull', function () {
|
||||
const input = this.blocks.statementInput2.inputList[0];
|
||||
const inNode = this.navigator.getFirstChild(input.connection);
|
||||
assert.isNull(inNode);
|
||||
});
|
||||
test('fromInputToPrevious', function () {
|
||||
const input = this.blocks.statementInput2.inputList[1];
|
||||
const previousConnection =
|
||||
this.blocks.statementInput3.previousConnection;
|
||||
const inNode = this.navigator.getFirstChild(input.connection);
|
||||
assert.equal(inNode, previousConnection);
|
||||
});
|
||||
test('fromBlockToInput', function () {
|
||||
const input = this.blocks.valueInput.inputList[0];
|
||||
const inNode = this.navigator.getFirstChild(this.blocks.valueInput);
|
||||
assert.equal(inNode, input.connection);
|
||||
});
|
||||
test('fromBlockToField', function () {
|
||||
const inNode = this.navigator.getFirstChild(
|
||||
this.blocks.statementInput1,
|
||||
);
|
||||
const field = this.blocks.statementInput1.inputList[0].fieldRow[0];
|
||||
assert.equal(inNode, field);
|
||||
});
|
||||
test('fromBlockToNull_DummyInput', function () {
|
||||
const inNode = this.navigator.getFirstChild(this.blocks.dummyInput);
|
||||
assert.isNull(inNode);
|
||||
});
|
||||
test('fromBlockToInput_DummyInputValue', function () {
|
||||
const inputConnection =
|
||||
this.blocks.dummyInputValue.inputList[1].connection;
|
||||
const inNode = this.navigator.getFirstChild(
|
||||
this.blocks.dummyInputValue,
|
||||
);
|
||||
assert.equal(inNode, inputConnection);
|
||||
});
|
||||
test('fromOuputToNull', function () {
|
||||
const output = this.blocks.fieldWithOutput.outputConnection;
|
||||
const inNode = this.navigator.getFirstChild(output);
|
||||
assert.isNull(inNode);
|
||||
});
|
||||
test('fromFieldToNull', function () {
|
||||
const field = this.blocks.statementInput1.inputList[0].fieldRow[0];
|
||||
const inNode = this.navigator.getFirstChild(field);
|
||||
assert.isNull(inNode);
|
||||
});
|
||||
test('fromWorkspaceToBlock', function () {
|
||||
const inNode = this.navigator.getFirstChild(this.workspace);
|
||||
assert.equal(inNode, this.workspace.getTopBlocks(true)[0]);
|
||||
});
|
||||
test('fromWorkspaceToNull', function () {
|
||||
const inNode = this.navigator.getFirstChild(this.emptyWorkspace);
|
||||
assert.isNull(inNode);
|
||||
});
|
||||
});
|
||||
|
||||
suite('Out', function () {
|
||||
setup(function () {
|
||||
const secondBlock = this.blocks.secondBlock;
|
||||
const outputNextBlock = this.blocks.outputNextBlock;
|
||||
this.blocks.noPrevConnection.nextConnection.connect(
|
||||
secondBlock.previousConnection,
|
||||
);
|
||||
secondBlock.inputList[0].connection.connect(
|
||||
outputNextBlock.outputConnection,
|
||||
);
|
||||
});
|
||||
|
||||
test('fromInputToBlock', function () {
|
||||
const input = this.blocks.statementInput1.inputList[0];
|
||||
const outNode = this.navigator.getParent(input.connection);
|
||||
assert.equal(outNode, this.blocks.statementInput1);
|
||||
});
|
||||
test('fromOutputToInput', function () {
|
||||
const output = this.blocks.fieldWithOutput.outputConnection;
|
||||
const outNode = this.navigator.getParent(output);
|
||||
assert.equal(
|
||||
outNode,
|
||||
this.blocks.statementInput1.inputList[0].connection,
|
||||
);
|
||||
});
|
||||
test('fromOutputToBlock', function () {
|
||||
const output = this.blocks.fieldWithOutput2.outputConnection;
|
||||
const outNode = this.navigator.getParent(output);
|
||||
assert.equal(outNode, this.blocks.fieldWithOutput2);
|
||||
});
|
||||
test('fromFieldToBlock', function () {
|
||||
const field = this.blocks.statementInput1.inputList[0].fieldRow[0];
|
||||
const outNode = this.navigator.getParent(field);
|
||||
assert.equal(outNode, this.blocks.statementInput1);
|
||||
});
|
||||
test('fromPreviousToInput', function () {
|
||||
const previous = this.blocks.statementInput3.previousConnection;
|
||||
const inputConnection =
|
||||
this.blocks.statementInput2.inputList[1].connection;
|
||||
const outNode = this.navigator.getParent(previous);
|
||||
assert.equal(outNode, inputConnection);
|
||||
});
|
||||
test('fromPreviousToBlock', function () {
|
||||
const previous = this.blocks.statementInput2.previousConnection;
|
||||
const outNode = this.navigator.getParent(previous);
|
||||
assert.equal(outNode, this.blocks.statementInput1);
|
||||
});
|
||||
test('fromNextToInput', function () {
|
||||
const next = this.blocks.statementInput3.nextConnection;
|
||||
const inputConnection =
|
||||
this.blocks.statementInput2.inputList[1].connection;
|
||||
const outNode = this.navigator.getParent(next);
|
||||
assert.equal(outNode, inputConnection);
|
||||
});
|
||||
test('fromNextToBlock', function () {
|
||||
const next = this.blocks.statementInput2.nextConnection;
|
||||
const outNode = this.navigator.getParent(next);
|
||||
assert.equal(outNode, this.blocks.statementInput1);
|
||||
});
|
||||
test('fromNextToBlock_NoPreviousConnection', function () {
|
||||
const next = this.blocks.secondBlock.nextConnection;
|
||||
const outNode = this.navigator.getParent(next);
|
||||
assert.equal(outNode, this.blocks.noPrevConnection);
|
||||
});
|
||||
/**
|
||||
* This is where there is a block with both an output connection and a
|
||||
* next connection attached to an input.
|
||||
*/
|
||||
test('fromNextToInput_OutputAndPreviousConnection', function () {
|
||||
const next = this.blocks.outputNextBlock.nextConnection;
|
||||
const outNode = this.navigator.getParent(next);
|
||||
assert.equal(outNode, this.blocks.secondBlock.inputList[0].connection);
|
||||
});
|
||||
test('fromBlockToStack', function () {
|
||||
const outNode = this.navigator.getParent(this.blocks.statementInput2);
|
||||
assert.equal(outNode, this.blocks.statementInput1);
|
||||
});
|
||||
test('fromBlockToInput', function () {
|
||||
const input = this.blocks.statementInput2.inputList[1].connection;
|
||||
const outNode = this.navigator.getParent(this.blocks.statementInput3);
|
||||
assert.equal(outNode, input);
|
||||
});
|
||||
test('fromTopBlockToStack', function () {
|
||||
const outNode = this.navigator.getParent(this.blocks.statementInput1);
|
||||
assert.equal(outNode, this.blocks.statementInput1);
|
||||
});
|
||||
test('fromBlockToStack_OutputConnection', function () {
|
||||
const outNode = this.navigator.getParent(this.blocks.fieldWithOutput2);
|
||||
assert.equal(outNode, this.blocks.fieldWithOutput2);
|
||||
});
|
||||
test('fromBlockToInput_OutputConnection', function () {
|
||||
const inputConnection = this.blocks.secondBlock.inputList[0].connection;
|
||||
const outNode = this.navigator.getParent(this.blocks.outputNextBlock);
|
||||
assert.equal(outNode, inputConnection);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -61,6 +61,10 @@ class FieldMitosis extends Field<CellGroup> {
|
||||
});
|
||||
this.value_ = {cells};
|
||||
}
|
||||
|
||||
getClass() {
|
||||
return FieldMitosis;
|
||||
}
|
||||
}
|
||||
|
||||
fieldRegistry.register('field_mitosis', FieldMitosis);
|
||||
|
||||
Reference in New Issue
Block a user