refactor: Remove INavigable in favor of IFocusableNode. (#9037)

* refactor: Remove INavigable in favor of IFocusableNode.

* chore: Fix JSDoc.

* chore: Address review feedback.
This commit is contained in:
Aaron Dodson
2025-05-13 15:04:49 -07:00
committed by GitHub
parent e34a9690ed
commit ae22165cbe
29 changed files with 236 additions and 393 deletions

View File

@@ -47,7 +47,6 @@ import type {IDragStrategy, IDraggable} from './interfaces/i_draggable.js';
import type {IFocusableNode} from './interfaces/i_focusable_node.js'; import type {IFocusableNode} from './interfaces/i_focusable_node.js';
import type {IFocusableTree} from './interfaces/i_focusable_tree.js'; import type {IFocusableTree} from './interfaces/i_focusable_tree.js';
import {IIcon} from './interfaces/i_icon.js'; import {IIcon} from './interfaces/i_icon.js';
import type {INavigable} from './interfaces/i_navigable.js';
import * as internalConstants from './internal_constants.js'; import * as internalConstants from './internal_constants.js';
import {Msg} from './msg.js'; import {Msg} from './msg.js';
import * as renderManagement from './render_management.js'; import * as renderManagement from './render_management.js';
@@ -77,8 +76,7 @@ export class BlockSvg
ICopyable<BlockCopyData>, ICopyable<BlockCopyData>,
IDraggable, IDraggable,
IDeletable, IDeletable,
IFocusableNode, IFocusableNode
INavigable<BlockSvg>
{ {
/** /**
* Constant for identifying rows that are to be rendered inline. * Constant for identifying rows that are to be rendered inline.
@@ -1796,16 +1794,4 @@ export class BlockSvg
canBeFocused(): boolean { canBeFocused(): boolean {
return true; 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;
}
} }

View File

@@ -171,7 +171,7 @@ import {
import {IVariableMap} from './interfaces/i_variable_map.js'; import {IVariableMap} from './interfaces/i_variable_map.js';
import {IVariableModel, IVariableState} from './interfaces/i_variable_model.js'; import {IVariableModel, IVariableState} from './interfaces/i_variable_model.js';
import * as internalConstants from './internal_constants.js'; import * as internalConstants from './internal_constants.js';
import {CursorOptions, LineCursor} from './keyboard_nav/line_cursor.js'; import {LineCursor} from './keyboard_nav/line_cursor.js';
import {Marker} from './keyboard_nav/marker.js'; import {Marker} from './keyboard_nav/marker.js';
import type {LayerManager} from './layer_manager.js'; import type {LayerManager} from './layer_manager.js';
import * as layers from './layers.js'; import * as layers from './layers.js';
@@ -429,7 +429,7 @@ Names.prototype.populateProcedures = function (
}; };
// clang-format on // clang-format on
export * from './interfaces/i_navigable.js'; export * from './flyout_navigator.js';
export * from './interfaces/i_navigation_policy.js'; export * from './interfaces/i_navigation_policy.js';
export * from './keyboard_nav/block_navigation_policy.js'; export * from './keyboard_nav/block_navigation_policy.js';
export * from './keyboard_nav/connection_navigation_policy.js'; export * from './keyboard_nav/connection_navigation_policy.js';
@@ -457,7 +457,6 @@ export {
ContextMenuItems, ContextMenuItems,
ContextMenuRegistry, ContextMenuRegistry,
Css, Css,
CursorOptions,
DeleteArea, DeleteArea,
DragTarget, DragTarget,
Events, Events,

View File

@@ -26,7 +26,6 @@ import type {Input} from './inputs/input.js';
import type {IFocusableNode} from './interfaces/i_focusable_node.js'; import type {IFocusableNode} from './interfaces/i_focusable_node.js';
import type {IFocusableTree} from './interfaces/i_focusable_tree.js'; import type {IFocusableTree} from './interfaces/i_focusable_tree.js';
import type {IKeyboardAccessible} from './interfaces/i_keyboard_accessible.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 type {IRegistrable} from './interfaces/i_registrable.js';
import {ISerializable} from './interfaces/i_serializable.js'; import {ISerializable} from './interfaces/i_serializable.js';
import type {ConstantProvider} from './renderers/common/constants.js'; import type {ConstantProvider} from './renderers/common/constants.js';
@@ -68,12 +67,7 @@ export type FieldValidator<T = any> = (newValue: T) => T | null | undefined;
* @typeParam T - The value stored on the field. * @typeParam T - The value stored on the field.
*/ */
export abstract class Field<T = any> export abstract class Field<T = any>
implements implements IKeyboardAccessible, IRegistrable, ISerializable, IFocusableNode
IKeyboardAccessible,
IRegistrable,
ISerializable,
IFocusableNode,
INavigable<Field<T>>
{ {
/** /**
* To overwrite the default value which is set in **Field**, directly update * To overwrite the default value which is set in **Field**, directly update
@@ -1410,16 +1404,6 @@ export abstract class Field<T = any>
`Attempted to instantiate a field from the registry that hasn't defined a 'fromJson' method.`, `Attempted to instantiate a field from the registry that hasn't defined a 'fromJson' method.`,
); );
} }
/**
* 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>;
} }
/** /**

View File

@@ -228,18 +228,6 @@ export class FieldCheckbox extends Field<CheckboxBool> {
// 'override' the static fromJson method. // 'override' the static fromJson method.
return new this(options.checked, undefined, options); 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); fieldRegistry.register('field_checkbox', FieldCheckbox);

View File

@@ -796,18 +796,6 @@ export class FieldDropdown extends Field<string> {
throw TypeError('Found invalid FieldDropdown options.'); 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;
}
} }
/** /**

View File

@@ -272,18 +272,6 @@ export class FieldImage extends Field<string> {
options, 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); fieldRegistry.register('field_image', FieldImage);

View File

@@ -126,18 +126,6 @@ export class FieldLabel extends Field<string> {
// the static fromJson method. // the static fromJson method.
return new this(text, undefined, options); 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); fieldRegistry.register('field_label', FieldLabel);

View File

@@ -341,18 +341,6 @@ export class FieldNumber extends FieldInput<number> {
options, 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); fieldRegistry.register('field_number', FieldNumber);

View File

@@ -89,18 +89,6 @@ export class FieldTextInput extends FieldInput<string> {
// override the static fromJson method. // override the static fromJson method.
return new this(text, undefined, options); 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); fieldRegistry.register('field_input', FieldTextInput);

View File

@@ -20,6 +20,7 @@ import {EventType} from './events/type.js';
import * as eventUtils from './events/utils.js'; import * as eventUtils from './events/utils.js';
import {FlyoutItem} from './flyout_item.js'; import {FlyoutItem} from './flyout_item.js';
import {FlyoutMetricsManager} from './flyout_metrics_manager.js'; import {FlyoutMetricsManager} from './flyout_metrics_manager.js';
import {FlyoutNavigator} from './flyout_navigator.js';
import {FlyoutSeparator, SeparatorAxis} from './flyout_separator.js'; import {FlyoutSeparator, SeparatorAxis} from './flyout_separator.js';
import {getFocusManager} from './focus_manager.js'; import {getFocusManager} from './focus_manager.js';
import {IAutoHideable} from './interfaces/i_autohideable.js'; import {IAutoHideable} from './interfaces/i_autohideable.js';
@@ -243,6 +244,7 @@ export abstract class Flyout
this.workspace_.internalIsFlyout = true; this.workspace_.internalIsFlyout = true;
// Keep the workspace visibility consistent with the flyout's visibility. // Keep the workspace visibility consistent with the flyout's visibility.
this.workspace_.setVisible(this.visible); this.workspace_.setVisible(this.visible);
this.workspace_.setNavigator(new FlyoutNavigator(this));
/** /**
* The unique id for this component that is used to register with the * The unique id for this component that is used to register with the

View File

@@ -16,7 +16,6 @@ import * as Css from './css.js';
import type {IBoundedElement} from './interfaces/i_bounded_element.js'; import type {IBoundedElement} from './interfaces/i_bounded_element.js';
import type {IFocusableNode} from './interfaces/i_focusable_node.js'; import type {IFocusableNode} from './interfaces/i_focusable_node.js';
import type {IFocusableTree} from './interfaces/i_focusable_tree.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 type {IRenderedElement} from './interfaces/i_rendered_element.js';
import {idGenerator} from './utils.js'; import {idGenerator} from './utils.js';
import {Coordinate} from './utils/coordinate.js'; import {Coordinate} from './utils/coordinate.js';
@@ -32,11 +31,7 @@ import type {WorkspaceSvg} from './workspace_svg.js';
* Class for a button or label in the flyout. * Class for a button or label in the flyout.
*/ */
export class FlyoutButton export class FlyoutButton
implements implements IBoundedElement, IRenderedElement, IFocusableNode
IBoundedElement,
IRenderedElement,
IFocusableNode,
INavigable<FlyoutButton>
{ {
/** The horizontal margin around the text in the button. */ /** The horizontal margin around the text in the button. */
static TEXT_MARGIN_X = 5; static TEXT_MARGIN_X = 5;
@@ -412,18 +407,6 @@ export class FlyoutButton
canBeFocused(): boolean { canBeFocused(): boolean {
return true; 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. */ /** CSS for buttons and labels. See css.js for use. */

View File

@@ -1,5 +1,6 @@
import type {IBoundedElement} from './interfaces/i_bounded_element.js'; import type {IBoundedElement} from './interfaces/i_bounded_element.js';
import type {INavigable} from './interfaces/i_navigable.js'; import type {IFocusableNode} from './interfaces/i_focusable_node.js';
/** /**
* Representation of an item displayed in a flyout. * Representation of an item displayed in a flyout.
*/ */
@@ -12,7 +13,7 @@ export class FlyoutItem {
* flyout inflater that created this object. * flyout inflater that created this object.
*/ */
constructor( constructor(
private element: IBoundedElement & INavigable<any>, private element: IBoundedElement & IFocusableNode,
private type: string, private type: string,
) {} ) {}

24
core/flyout_navigator.ts Normal file
View File

@@ -0,0 +1,24 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type {IFlyout} from './interfaces/i_flyout.js';
import {FlyoutButtonNavigationPolicy} from './keyboard_nav/flyout_button_navigation_policy.js';
import {FlyoutNavigationPolicy} from './keyboard_nav/flyout_navigation_policy.js';
import {FlyoutSeparatorNavigationPolicy} from './keyboard_nav/flyout_separator_navigation_policy.js';
import {Navigator} from './navigator.js';
export class FlyoutNavigator extends Navigator {
constructor(flyout: IFlyout) {
super();
this.rules.push(
new FlyoutButtonNavigationPolicy(),
new FlyoutSeparatorNavigationPolicy(),
);
this.rules = this.rules.map(
(rule) => new FlyoutNavigationPolicy(rule, flyout),
);
}
}

View File

@@ -7,15 +7,12 @@
import type {IBoundedElement} from './interfaces/i_bounded_element.js'; import type {IBoundedElement} from './interfaces/i_bounded_element.js';
import type {IFocusableNode} from './interfaces/i_focusable_node.js'; import type {IFocusableNode} from './interfaces/i_focusable_node.js';
import type {IFocusableTree} from './interfaces/i_focusable_tree.js'; import type {IFocusableTree} from './interfaces/i_focusable_tree.js';
import type {INavigable} from './interfaces/i_navigable.js';
import {Rect} from './utils/rect.js'; import {Rect} from './utils/rect.js';
/** /**
* Representation of a gap between elements in a flyout. * Representation of a gap between elements in a flyout.
*/ */
export class FlyoutSeparator export class FlyoutSeparator implements IBoundedElement, IFocusableNode {
implements IBoundedElement, INavigable<FlyoutSeparator>, IFocusableNode
{
private x = 0; private x = 0;
private y = 0; private y = 0;
@@ -66,18 +63,6 @@ export class FlyoutSeparator
return false; 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;
}
/** See IFocusableNode.getFocusableElement. */ /** See IFocusableNode.getFocusableElement. */
getFocusableElement(): HTMLElement | SVGElement { getFocusableElement(): HTMLElement | SVGElement {
throw new Error('Cannot be focused'); throw new Error('Cannot be focused');

View File

@@ -1,19 +0,0 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type {IFocusableNode} from './i_focusable_node.js';
/**
* Represents a UI element which can be navigated to using the keyboard.
*/
export interface INavigable<T> extends IFocusableNode {
/**
* Returns the class of this instance.
*
* @returns This object's class.
*/
getClass(): new (...args: any) => T;
}

View File

@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
import type {INavigable} from './i_navigable.js'; import type {IFocusableNode} from './i_focusable_node.js';
/** /**
* A set of rules that specify where keyboard navigation should proceed. * A set of rules that specify where keyboard navigation should proceed.
@@ -16,7 +16,7 @@ export interface INavigationPolicy<T> {
* @param current The element which the user is navigating into. * @param current The element which the user is navigating into.
* @returns The current element's first child, or null if it has none. * @returns The current element's first child, or null if it has none.
*/ */
getFirstChild(current: T): INavigable<any> | null; getFirstChild(current: T): IFocusableNode | null;
/** /**
* Returns the parent element of the given element, if any. * Returns the parent element of the given element, if any.
@@ -24,7 +24,7 @@ export interface INavigationPolicy<T> {
* @param current The element which the user is navigating out of. * @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. * @returns The parent element of the current element, or null if it has none.
*/ */
getParent(current: T): INavigable<any> | null; getParent(current: T): IFocusableNode | null;
/** /**
* Returns the peer element following the given element, if any. * Returns the peer element following the given element, if any.
@@ -33,7 +33,7 @@ export interface INavigationPolicy<T> {
* @returns The next peer element of the current element, or null if there is * @returns The next peer element of the current element, or null if there is
* none. * none.
*/ */
getNextSibling(current: T): INavigable<any> | null; getNextSibling(current: T): IFocusableNode | null;
/** /**
* Returns the peer element preceding the given element, if any. * Returns the peer element preceding the given element, if any.
@@ -42,7 +42,7 @@ export interface INavigationPolicy<T> {
* @returns The previous peer element of the current element, or null if * @returns The previous peer element of the current element, or null if
* there is none. * there is none.
*/ */
getPreviousSibling(current: T): INavigable<any> | null; getPreviousSibling(current: T): IFocusableNode | null;
/** /**
* Returns whether or not the given instance should be reachable via keyboard * Returns whether or not the given instance should be reachable via keyboard
@@ -57,4 +57,13 @@ export interface INavigationPolicy<T> {
* @returns True if this element should be included in keyboard navigation. * @returns True if this element should be included in keyboard navigation.
*/ */
isNavigable(current: T): boolean; isNavigable(current: T): boolean;
/**
* Returns whether or not this navigation policy corresponds to the type of
* the given object.
*
* @param current An instance to check whether this policy applies to.
* @returns True if the given object is of a type handled by this policy.
*/
isApplicable(current: any): current is T;
} }

View File

@@ -6,7 +6,7 @@
import {BlockSvg} from '../block_svg.js'; import {BlockSvg} from '../block_svg.js';
import type {Field} from '../field.js'; import type {Field} from '../field.js';
import type {INavigable} from '../interfaces/i_navigable.js'; import type {IFocusableNode} from '../interfaces/i_focusable_node.js';
import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js'; import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js';
import {WorkspaceSvg} from '../workspace_svg.js'; import {WorkspaceSvg} from '../workspace_svg.js';
@@ -20,7 +20,7 @@ export class BlockNavigationPolicy implements INavigationPolicy<BlockSvg> {
* @param current The block to return the first child of. * @param current The block to return the first child of.
* @returns The first field or input of the given block, if any. * @returns The first field or input of the given block, if any.
*/ */
getFirstChild(current: BlockSvg): INavigable<unknown> | null { getFirstChild(current: BlockSvg): IFocusableNode | null {
for (const input of current.inputList) { for (const input of current.inputList) {
for (const field of input.fieldRow) { for (const field of input.fieldRow) {
return field; return field;
@@ -39,7 +39,7 @@ export class BlockNavigationPolicy implements INavigationPolicy<BlockSvg> {
* @returns The top block of the given block's stack, or the connection to * @returns The top block of the given block's stack, or the connection to
* which it is attached. * which it is attached.
*/ */
getParent(current: BlockSvg): INavigable<unknown> | null { getParent(current: BlockSvg): IFocusableNode | null {
if (current.previousConnection?.targetBlock()) { if (current.previousConnection?.targetBlock()) {
const surroundParent = current.getSurroundParent(); const surroundParent = current.getSurroundParent();
if (surroundParent) return surroundParent; if (surroundParent) return surroundParent;
@@ -57,7 +57,7 @@ export class BlockNavigationPolicy implements INavigationPolicy<BlockSvg> {
* @returns The first block of the next stack if the given block is a terminal * @returns The first block of the next stack if the given block is a terminal
* block, or its next connection. * block, or its next connection.
*/ */
getNextSibling(current: BlockSvg): INavigable<unknown> | null { getNextSibling(current: BlockSvg): IFocusableNode | null {
if (current.nextConnection?.targetBlock()) { if (current.nextConnection?.targetBlock()) {
return current.nextConnection?.targetBlock(); return current.nextConnection?.targetBlock();
} }
@@ -101,7 +101,7 @@ export class BlockNavigationPolicy implements INavigationPolicy<BlockSvg> {
* @returns The block's previous/output connection, or the last * @returns The block's previous/output connection, or the last
* connection/block of the previous block stack if it is a root block. * connection/block of the previous block stack if it is a root block.
*/ */
getPreviousSibling(current: BlockSvg): INavigable<unknown> | null { getPreviousSibling(current: BlockSvg): IFocusableNode | null {
if (current.previousConnection?.targetBlock()) { if (current.previousConnection?.targetBlock()) {
return current.previousConnection?.targetBlock(); return current.previousConnection?.targetBlock();
} }
@@ -127,7 +127,7 @@ export class BlockNavigationPolicy implements INavigationPolicy<BlockSvg> {
} }
const currentIndex = siblings.indexOf(current); const currentIndex = siblings.indexOf(current);
let result: INavigable<any> | null = null; let result: IFocusableNode | null = null;
if (currentIndex >= 1) { if (currentIndex >= 1) {
result = siblings[currentIndex - 1]; result = siblings[currentIndex - 1];
} else if (currentIndex === 0 && navigatingCrossStacks) { } else if (currentIndex === 0 && navigatingCrossStacks) {
@@ -152,4 +152,14 @@ export class BlockNavigationPolicy implements INavigationPolicy<BlockSvg> {
isNavigable(current: BlockSvg): boolean { isNavigable(current: BlockSvg): boolean {
return current.canBeFocused(); return current.canBeFocused();
} }
/**
* Returns whether the given object can be navigated from by this policy.
*
* @param current The object to check if this policy applies to.
* @returns True if the object is a BlockSvg.
*/
isApplicable(current: any): current is BlockSvg {
return current instanceof BlockSvg;
}
} }

View File

@@ -6,9 +6,9 @@
import type {BlockSvg} from '../block_svg.js'; import type {BlockSvg} from '../block_svg.js';
import {ConnectionType} from '../connection_type.js'; import {ConnectionType} from '../connection_type.js';
import type {INavigable} from '../interfaces/i_navigable.js'; import type {IFocusableNode} from '../interfaces/i_focusable_node.js';
import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js'; import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js';
import type {RenderedConnection} from '../rendered_connection.js'; import {RenderedConnection} from '../rendered_connection.js';
/** /**
* Set of rules controlling keyboard navigation from a connection. * Set of rules controlling keyboard navigation from a connection.
@@ -22,7 +22,7 @@ export class ConnectionNavigationPolicy
* @param current The connection to return the first child of. * @param current The connection to return the first child of.
* @returns The connection's first child element, or null if not none. * @returns The connection's first child element, or null if not none.
*/ */
getFirstChild(current: RenderedConnection): INavigable<unknown> | null { getFirstChild(current: RenderedConnection): IFocusableNode | null {
if (current.getParentInput()) { if (current.getParentInput()) {
return current.targetConnection; return current.targetConnection;
} }
@@ -36,7 +36,7 @@ export class ConnectionNavigationPolicy
* @param current The connection to return the parent of. * @param current The connection to return the parent of.
* @returns The given connection's parent connection or block. * @returns The given connection's parent connection or block.
*/ */
getParent(current: RenderedConnection): INavigable<unknown> | null { getParent(current: RenderedConnection): IFocusableNode | null {
if (current.type === ConnectionType.OUTPUT_VALUE) { if (current.type === ConnectionType.OUTPUT_VALUE) {
return current.targetConnection ?? current.getSourceBlock(); return current.targetConnection ?? current.getSourceBlock();
} else if (current.getParentInput()) { } else if (current.getParentInput()) {
@@ -56,7 +56,7 @@ export class ConnectionNavigationPolicy
* @param current The connection to navigate from. * @param current The connection to navigate from.
* @returns The field, input connection or block following this connection. * @returns The field, input connection or block following this connection.
*/ */
getNextSibling(current: RenderedConnection): INavigable<unknown> | null { getNextSibling(current: RenderedConnection): IFocusableNode | null {
if (current.getParentInput()) { if (current.getParentInput()) {
const parentInput = current.getParentInput(); const parentInput = current.getParentInput();
const block = parentInput?.getSourceBlock(); const block = parentInput?.getSourceBlock();
@@ -101,7 +101,7 @@ export class ConnectionNavigationPolicy
* @param current The connection to navigate from. * @param current The connection to navigate from.
* @returns The field, input connection or block preceding this connection. * @returns The field, input connection or block preceding this connection.
*/ */
getPreviousSibling(current: RenderedConnection): INavigable<unknown> | null { getPreviousSibling(current: RenderedConnection): IFocusableNode | null {
if (current.getParentInput()) { if (current.getParentInput()) {
const parentInput = current.getParentInput(); const parentInput = current.getParentInput();
const block = parentInput?.getSourceBlock(); const block = parentInput?.getSourceBlock();
@@ -176,4 +176,14 @@ export class ConnectionNavigationPolicy
isNavigable(current: RenderedConnection): boolean { isNavigable(current: RenderedConnection): boolean {
return current.canBeFocused(); return current.canBeFocused();
} }
/**
* Returns whether the given object can be navigated from by this policy.
*
* @param current The object to check if this policy applies to.
* @returns True if the object is a RenderedConnection.
*/
isApplicable(current: any): current is RenderedConnection {
return current instanceof RenderedConnection;
}
} }

View File

@@ -5,8 +5,8 @@
*/ */
import type {BlockSvg} from '../block_svg.js'; import type {BlockSvg} from '../block_svg.js';
import type {Field} from '../field.js'; import {Field} from '../field.js';
import type {INavigable} from '../interfaces/i_navigable.js'; import type {IFocusableNode} from '../interfaces/i_focusable_node.js';
import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js'; import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js';
/** /**
@@ -19,7 +19,7 @@ export class FieldNavigationPolicy implements INavigationPolicy<Field<any>> {
* @param _current The field to navigate from. * @param _current The field to navigate from.
* @returns Null. * @returns Null.
*/ */
getFirstChild(_current: Field<any>): INavigable<unknown> | null { getFirstChild(_current: Field<any>): IFocusableNode | null {
return null; return null;
} }
@@ -29,7 +29,7 @@ export class FieldNavigationPolicy implements INavigationPolicy<Field<any>> {
* @param current The field to navigate from. * @param current The field to navigate from.
* @returns The given field's parent block. * @returns The given field's parent block.
*/ */
getParent(current: Field<any>): INavigable<unknown> | null { getParent(current: Field<any>): IFocusableNode | null {
return current.getSourceBlock() as BlockSvg; return current.getSourceBlock() as BlockSvg;
} }
@@ -39,7 +39,7 @@ export class FieldNavigationPolicy implements INavigationPolicy<Field<any>> {
* @param current The field to navigate from. * @param current The field to navigate from.
* @returns The next field or input in the given field's block. * @returns The next field or input in the given field's block.
*/ */
getNextSibling(current: Field<any>): INavigable<unknown> | null { getNextSibling(current: Field<any>): IFocusableNode | null {
const input = current.getParentInput(); const input = current.getParentInput();
const block = current.getSourceBlock(); const block = current.getSourceBlock();
if (!block) return null; if (!block) return null;
@@ -64,7 +64,7 @@ export class FieldNavigationPolicy implements INavigationPolicy<Field<any>> {
* @param current The field to navigate from. * @param current The field to navigate from.
* @returns The preceding field or input in the given field's block. * @returns The preceding field or input in the given field's block.
*/ */
getPreviousSibling(current: Field<any>): INavigable<unknown> | null { getPreviousSibling(current: Field<any>): IFocusableNode | null {
const parentInput = current.getParentInput(); const parentInput = current.getParentInput();
const block = current.getSourceBlock(); const block = current.getSourceBlock();
if (!block) return null; if (!block) return null;
@@ -106,4 +106,14 @@ export class FieldNavigationPolicy implements INavigationPolicy<Field<any>> {
current.getParentInput().isVisible() current.getParentInput().isVisible()
); );
} }
/**
* Returns whether the given object can be navigated from by this policy.
*
* @param current The object to check if this policy applies to.
* @returns True if the object is a Field.
*/
isApplicable(current: any): current is Field {
return current instanceof Field;
}
} }

View File

@@ -4,8 +4,8 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
import type {FlyoutButton} from '../flyout_button.js'; import {FlyoutButton} from '../flyout_button.js';
import type {INavigable} from '../interfaces/i_navigable.js'; import type {IFocusableNode} from '../interfaces/i_focusable_node.js';
import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js'; import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js';
/** /**
@@ -20,7 +20,7 @@ export class FlyoutButtonNavigationPolicy
* @param _current The FlyoutButton instance to navigate from. * @param _current The FlyoutButton instance to navigate from.
* @returns Null. * @returns Null.
*/ */
getFirstChild(_current: FlyoutButton): INavigable<unknown> | null { getFirstChild(_current: FlyoutButton): IFocusableNode | null {
return null; return null;
} }
@@ -30,7 +30,7 @@ export class FlyoutButtonNavigationPolicy
* @param current The FlyoutButton instance to navigate from. * @param current The FlyoutButton instance to navigate from.
* @returns The given flyout button's parent workspace. * @returns The given flyout button's parent workspace.
*/ */
getParent(current: FlyoutButton): INavigable<unknown> | null { getParent(current: FlyoutButton): IFocusableNode | null {
return current.getWorkspace(); return current.getWorkspace();
} }
@@ -40,7 +40,7 @@ export class FlyoutButtonNavigationPolicy
* @param _current The FlyoutButton instance to navigate from. * @param _current The FlyoutButton instance to navigate from.
* @returns Null. * @returns Null.
*/ */
getNextSibling(_current: FlyoutButton): INavigable<unknown> | null { getNextSibling(_current: FlyoutButton): IFocusableNode | null {
return null; return null;
} }
@@ -50,7 +50,7 @@ export class FlyoutButtonNavigationPolicy
* @param _current The FlyoutButton instance to navigate from. * @param _current The FlyoutButton instance to navigate from.
* @returns Null. * @returns Null.
*/ */
getPreviousSibling(_current: FlyoutButton): INavigable<unknown> | null { getPreviousSibling(_current: FlyoutButton): IFocusableNode | null {
return null; return null;
} }
@@ -63,4 +63,14 @@ export class FlyoutButtonNavigationPolicy
isNavigable(current: FlyoutButton): boolean { isNavigable(current: FlyoutButton): boolean {
return current.canBeFocused(); return current.canBeFocused();
} }
/**
* Returns whether the given object can be navigated from by this policy.
*
* @param current The object to check if this policy applies to.
* @returns True if the object is a FlyoutButton.
*/
isApplicable(current: any): current is FlyoutButton {
return current instanceof FlyoutButton;
}
} }

View File

@@ -5,7 +5,7 @@
*/ */
import type {IFlyout} from '../interfaces/i_flyout.js'; import type {IFlyout} from '../interfaces/i_flyout.js';
import type {INavigable} from '../interfaces/i_navigable.js'; import type {IFocusableNode} from '../interfaces/i_focusable_node.js';
import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js'; import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js';
/** /**
@@ -29,7 +29,7 @@ export class FlyoutNavigationPolicy<T> implements INavigationPolicy<T> {
* @param _current The flyout item to navigate from. * @param _current The flyout item to navigate from.
* @returns Null to prevent navigating into flyout items. * @returns Null to prevent navigating into flyout items.
*/ */
getFirstChild(_current: T): INavigable<unknown> | null { getFirstChild(_current: T): IFocusableNode | null {
return null; return null;
} }
@@ -39,7 +39,7 @@ export class FlyoutNavigationPolicy<T> implements INavigationPolicy<T> {
* @param current The flyout item to navigate from. * @param current The flyout item to navigate from.
* @returns The parent of the given flyout item. * @returns The parent of the given flyout item.
*/ */
getParent(current: T): INavigable<unknown> | null { getParent(current: T): IFocusableNode | null {
return this.policy.getParent(current); return this.policy.getParent(current);
} }
@@ -49,7 +49,7 @@ export class FlyoutNavigationPolicy<T> implements INavigationPolicy<T> {
* @param current The flyout item to navigate from. * @param current The flyout item to navigate from.
* @returns The flyout item following the given one. * @returns The flyout item following the given one.
*/ */
getNextSibling(current: T): INavigable<unknown> | null { getNextSibling(current: T): IFocusableNode | null {
const flyoutContents = this.flyout.getContents(); const flyoutContents = this.flyout.getContents();
if (!flyoutContents) return null; if (!flyoutContents) return null;
@@ -72,7 +72,7 @@ export class FlyoutNavigationPolicy<T> implements INavigationPolicy<T> {
* @param current The flyout item to navigate from. * @param current The flyout item to navigate from.
* @returns The flyout item preceding the given one. * @returns The flyout item preceding the given one.
*/ */
getPreviousSibling(current: T): INavigable<unknown> | null { getPreviousSibling(current: T): IFocusableNode | null {
const flyoutContents = this.flyout.getContents(); const flyoutContents = this.flyout.getContents();
if (!flyoutContents) return null; if (!flyoutContents) return null;
@@ -98,4 +98,14 @@ export class FlyoutNavigationPolicy<T> implements INavigationPolicy<T> {
isNavigable(current: T): boolean { isNavigable(current: T): boolean {
return this.policy.isNavigable(current); return this.policy.isNavigable(current);
} }
/**
* Returns whether the given object can be navigated from by this policy.
*
* @param current The object to check if this policy applies to.
* @returns True if the object is a BlockSvg.
*/
isApplicable(current: any): current is T {
return this.policy.isApplicable(current);
}
} }

View File

@@ -4,8 +4,8 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
import type {FlyoutSeparator} from '../flyout_separator.js'; import {FlyoutSeparator} from '../flyout_separator.js';
import type {INavigable} from '../interfaces/i_navigable.js'; import type {IFocusableNode} from '../interfaces/i_focusable_node.js';
import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js'; import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js';
/** /**
@@ -15,19 +15,19 @@ import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js';
export class FlyoutSeparatorNavigationPolicy export class FlyoutSeparatorNavigationPolicy
implements INavigationPolicy<FlyoutSeparator> implements INavigationPolicy<FlyoutSeparator>
{ {
getFirstChild(_current: FlyoutSeparator): INavigable<unknown> | null { getFirstChild(_current: FlyoutSeparator): IFocusableNode | null {
return null; return null;
} }
getParent(_current: FlyoutSeparator): INavigable<unknown> | null { getParent(_current: FlyoutSeparator): IFocusableNode | null {
return null; return null;
} }
getNextSibling(_current: FlyoutSeparator): INavigable<unknown> | null { getNextSibling(_current: FlyoutSeparator): IFocusableNode | null {
return null; return null;
} }
getPreviousSibling(_current: FlyoutSeparator): INavigable<unknown> | null { getPreviousSibling(_current: FlyoutSeparator): IFocusableNode | null {
return null; return null;
} }
@@ -40,4 +40,14 @@ export class FlyoutSeparatorNavigationPolicy
isNavigable(_current: FlyoutSeparator): boolean { isNavigable(_current: FlyoutSeparator): boolean {
return false; return false;
} }
/**
* Returns whether the given object can be navigated from by this policy.
*
* @param current The object to check if this policy applies to.
* @returns True if the object is a FlyoutSeparator.
*/
isApplicable(current: any): current is FlyoutSeparator {
return current instanceof FlyoutSeparator;
}
} }

View File

@@ -14,43 +14,12 @@
*/ */
import {BlockSvg} from '../block_svg.js'; import {BlockSvg} from '../block_svg.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 {getFocusManager} from '../focus_manager.js';
import type {IFocusableNode} from '../interfaces/i_focusable_node.js';
import {isFocusableNode} from '../interfaces/i_focusable_node.js'; import {isFocusableNode} from '../interfaces/i_focusable_node.js';
import type {INavigable} from '../interfaces/i_navigable.js';
import * as registry from '../registry.js'; import * as registry from '../registry.js';
import {RenderedConnection} from '../rendered_connection.js';
import {Renderer} from '../renderers/zelos/renderer.js';
import {WorkspaceSvg} from '../workspace_svg.js'; import {WorkspaceSvg} from '../workspace_svg.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 {Marker} from './marker.js';
import {WorkspaceNavigationPolicy} from './workspace_navigation_policy.js';
/** Options object for LineCursor instances. */
export interface CursorOptions {
/**
* Can the cursor visit all stack connections (next/previous), or
* (if false) only unconnected next connections?
*/
stackConnections: boolean;
}
/** Default options for LineCursor instances. */
const defaultOptions: CursorOptions = {
stackConnections: true,
};
/** /**
* Class for a line cursor. * Class for a line cursor.
@@ -58,76 +27,14 @@ const defaultOptions: CursorOptions = {
export class LineCursor extends Marker { export class LineCursor extends Marker {
override type = 'cursor'; override type = 'cursor';
/** Options for this line cursor. */
private readonly options: CursorOptions;
/** Locations to try moving the cursor to after a deletion. */ /** Locations to try moving the cursor to after a deletion. */
private potentialNodes: INavigable<any>[] | null = null; private potentialNodes: IFocusableNode[] | null = null;
/** Whether the renderer is zelos-style. */
private isZelos = false;
/** /**
* @param workspace The workspace this cursor belongs to. * @param workspace The workspace this cursor belongs to.
* @param options Cursor options.
*/ */
constructor( constructor(protected readonly workspace: WorkspaceSvg) {
protected readonly workspace: WorkspaceSvg,
options?: Partial<CursorOptions>,
) {
super(); super();
// Regularise options and apply defaults.
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);
} }
/** /**
@@ -137,14 +44,14 @@ export class LineCursor extends Marker {
* @returns The next node, or null if the current node is * @returns The next node, or null if the current node is
* not set or there is no next value. * not set or there is no next value.
*/ */
next(): INavigable<any> | null { next(): IFocusableNode | null {
const curNode = this.getCurNode(); const curNode = this.getCurNode();
if (!curNode) { if (!curNode) {
return null; return null;
} }
const newNode = this.getNextNode( const newNode = this.getNextNode(
curNode, curNode,
(candidate: INavigable<any> | null) => { (candidate: IFocusableNode | null) => {
return ( return (
candidate instanceof BlockSvg && candidate instanceof BlockSvg &&
!candidate.outputConnection?.targetBlock() !candidate.outputConnection?.targetBlock()
@@ -166,7 +73,7 @@ export class LineCursor extends Marker {
* @returns The next node, or null if the current node is * @returns The next node, or null if the current node is
* not set or there is no next value. * not set or there is no next value.
*/ */
in(): INavigable<any> | null { in(): IFocusableNode | null {
const curNode = this.getCurNode(); const curNode = this.getCurNode();
if (!curNode) { if (!curNode) {
return null; return null;
@@ -186,14 +93,14 @@ export class LineCursor extends Marker {
* @returns The previous node, or null if the current node * @returns The previous node, or null if the current node
* is not set or there is no previous value. * is not set or there is no previous value.
*/ */
prev(): INavigable<any> | null { prev(): IFocusableNode | null {
const curNode = this.getCurNode(); const curNode = this.getCurNode();
if (!curNode) { if (!curNode) {
return null; return null;
} }
const newNode = this.getPreviousNode( const newNode = this.getPreviousNode(
curNode, curNode,
(candidate: INavigable<any> | null) => { (candidate: IFocusableNode | null) => {
return ( return (
candidate instanceof BlockSvg && candidate instanceof BlockSvg &&
!candidate.outputConnection?.targetBlock() !candidate.outputConnection?.targetBlock()
@@ -215,7 +122,7 @@ export class LineCursor extends Marker {
* @returns The previous node, or null if the current node * @returns The previous node, or null if the current node
* is not set or there is no previous value. * is not set or there is no previous value.
*/ */
out(): INavigable<any> | null { out(): IFocusableNode | null {
const curNode = this.getCurNode(); const curNode = this.getCurNode();
if (!curNode) { if (!curNode) {
return null; return null;
@@ -241,7 +148,7 @@ export class LineCursor extends Marker {
const inNode = this.getNextNode(curNode, () => true, true); const inNode = this.getNextNode(curNode, () => true, true);
const nextNode = this.getNextNode( const nextNode = this.getNextNode(
curNode, curNode,
(candidate: INavigable<any> | null) => { (candidate: IFocusableNode | null) => {
return ( return (
candidate instanceof BlockSvg && candidate instanceof BlockSvg &&
!candidate.outputConnection?.targetBlock() !candidate.outputConnection?.targetBlock()
@@ -265,10 +172,10 @@ export class LineCursor extends Marker {
* @returns The next node in the traversal. * @returns The next node in the traversal.
*/ */
private getNextNodeImpl( private getNextNodeImpl(
node: INavigable<any> | null, node: IFocusableNode | null,
isValid: (p1: INavigable<any> | null) => boolean, isValid: (p1: IFocusableNode | null) => boolean,
visitedNodes: Set<INavigable<any>> = new Set<INavigable<any>>(), visitedNodes: Set<IFocusableNode> = new Set<IFocusableNode>(),
): INavigable<any> | null { ): IFocusableNode | null {
if (!node || visitedNodes.has(node)) return null; if (!node || visitedNodes.has(node)) return null;
let newNode = let newNode =
this.workspace.getNavigator().getFirstChild(node) || this.workspace.getNavigator().getFirstChild(node) ||
@@ -301,10 +208,10 @@ export class LineCursor extends Marker {
* @returns The next node in the traversal. * @returns The next node in the traversal.
*/ */
getNextNode( getNextNode(
node: INavigable<any> | null, node: IFocusableNode | null,
isValid: (p1: INavigable<any> | null) => boolean, isValid: (p1: IFocusableNode | null) => boolean,
loop: boolean, loop: boolean,
): INavigable<any> | null { ): IFocusableNode | null {
if (!node || (!loop && this.getLastNode() === node)) return null; if (!node || (!loop && this.getLastNode() === node)) return null;
return this.getNextNodeImpl(node, isValid); return this.getNextNodeImpl(node, isValid);
@@ -323,10 +230,10 @@ export class LineCursor extends Marker {
* exists. * exists.
*/ */
private getPreviousNodeImpl( private getPreviousNodeImpl(
node: INavigable<any> | null, node: IFocusableNode | null,
isValid: (p1: INavigable<any> | null) => boolean, isValid: (p1: IFocusableNode | null) => boolean,
visitedNodes: Set<INavigable<any>> = new Set<INavigable<any>>(), visitedNodes: Set<IFocusableNode> = new Set<IFocusableNode>(),
): INavigable<any> | null { ): IFocusableNode | null {
if (!node || visitedNodes.has(node)) return null; if (!node || visitedNodes.has(node)) return null;
const newNode = const newNode =
@@ -355,10 +262,10 @@ export class LineCursor extends Marker {
* exists. * exists.
*/ */
getPreviousNode( getPreviousNode(
node: INavigable<any> | null, node: IFocusableNode | null,
isValid: (p1: INavigable<any> | null) => boolean, isValid: (p1: IFocusableNode | null) => boolean,
loop: boolean, loop: boolean,
): INavigable<any> | null { ): IFocusableNode | null {
if (!node || (!loop && this.getFirstNode() === node)) return null; if (!node || (!loop && this.getFirstNode() === node)) return null;
return this.getPreviousNodeImpl(node, isValid); return this.getPreviousNodeImpl(node, isValid);
@@ -371,15 +278,15 @@ export class LineCursor extends Marker {
* @returns The right most child of the given node, or the node if no child * @returns The right most child of the given node, or the node if no child
* exists. * exists.
*/ */
getRightMostChild( private getRightMostChild(
node: INavigable<any> | null, node: IFocusableNode | null,
stopIfFound: INavigable<any>, stopIfFound: IFocusableNode,
): INavigable<any> | null { ): IFocusableNode | null {
if (!node) return node; if (!node) return node;
let newNode = this.workspace.getNavigator().getFirstChild(node); let newNode = this.workspace.getNavigator().getFirstChild(node);
if (!newNode || newNode === stopIfFound) return node; if (!newNode || newNode === stopIfFound) return node;
for ( for (
let nextNode: INavigable<any> | null = newNode; let nextNode: IFocusableNode | null = newNode;
nextNode; nextNode;
nextNode = this.workspace.getNavigator().getNextSibling(newNode) nextNode = this.workspace.getNavigator().getNextSibling(newNode)
) { ) {
@@ -414,7 +321,7 @@ export class LineCursor extends Marker {
preDelete(deletedBlock: BlockSvg) { preDelete(deletedBlock: BlockSvg) {
const curNode = this.getCurNode(); const curNode = this.getCurNode();
const nodes: INavigable<any>[] = curNode ? [curNode] : []; const nodes: IFocusableNode[] = curNode ? [curNode] : [];
// The connection to which the deleted block is attached. // The connection to which the deleted block is attached.
const parentConnection = const parentConnection =
deletedBlock.previousConnection?.targetConnection ?? deletedBlock.previousConnection?.targetConnection ??
@@ -466,7 +373,7 @@ export class LineCursor extends Marker {
* *
* @returns The current field, connection, or block the cursor is on. * @returns The current field, connection, or block the cursor is on.
*/ */
override getCurNode(): INavigable<any> | null { override getCurNode(): IFocusableNode | null {
this.updateCurNodeFromFocus(); this.updateCurNodeFromFocus();
return super.getCurNode(); return super.getCurNode();
} }
@@ -479,7 +386,7 @@ export class LineCursor extends Marker {
* *
* @param newNode The new location of the cursor. * @param newNode The new location of the cursor.
*/ */
override setCurNode(newNode: INavigable<any> | null) { override setCurNode(newNode: IFocusableNode | null) {
super.setCurNode(newNode); super.setCurNode(newNode);
if (isFocusableNode(newNode)) { if (isFocusableNode(newNode)) {
@@ -513,7 +420,7 @@ export class LineCursor extends Marker {
* *
* @returns The first navigable node on the workspace, or null. * @returns The first navigable node on the workspace, or null.
*/ */
getFirstNode(): INavigable<any> | null { getFirstNode(): IFocusableNode | null {
return this.workspace.getNavigator().getFirstChild(this.workspace); return this.workspace.getNavigator().getFirstChild(this.workspace);
} }
@@ -522,7 +429,7 @@ export class LineCursor extends Marker {
* *
* @returns The last navigable node on the workspace, or null. * @returns The last navigable node on the workspace, or null.
*/ */
getLastNode(): INavigable<any> | null { getLastNode(): IFocusableNode | null {
const first = this.getFirstNode(); const first = this.getFirstNode();
return this.getPreviousNode(first, () => true, true); return this.getPreviousNode(first, () => true, true);
} }

View File

@@ -14,7 +14,7 @@
import {BlockSvg} from '../block_svg.js'; import {BlockSvg} from '../block_svg.js';
import {Field} from '../field.js'; import {Field} from '../field.js';
import type {INavigable} from '../interfaces/i_navigable.js'; import type {IFocusableNode} from '../interfaces/i_focusable_node.js';
import {RenderedConnection} from '../rendered_connection.js'; import {RenderedConnection} from '../rendered_connection.js';
/** /**
@@ -26,7 +26,7 @@ export class Marker {
colour: string | null = null; colour: string | null = null;
/** The current location of the marker. */ /** The current location of the marker. */
protected curNode: INavigable<any> | null = null; protected curNode: IFocusableNode | null = null;
/** The type of the marker. */ /** The type of the marker. */
type = 'marker'; type = 'marker';
@@ -36,7 +36,7 @@ export class Marker {
* *
* @returns The current field, connection, or block the marker is on. * @returns The current field, connection, or block the marker is on.
*/ */
getCurNode(): INavigable<any> | null { getCurNode(): IFocusableNode | null {
return this.curNode; return this.curNode;
} }
@@ -45,7 +45,7 @@ export class Marker {
* *
* @param newNode The new location of the marker, or null to remove it. * @param newNode The new location of the marker, or null to remove it.
*/ */
setCurNode(newNode: INavigable<any> | null) { setCurNode(newNode: IFocusableNode | null) {
this.curNode = newNode; this.curNode = newNode;
} }
@@ -59,7 +59,7 @@ export class Marker {
* *
* @returns The parent block of the node if any, otherwise null. * @returns The parent block of the node if any, otherwise null.
*/ */
getSourceBlockFromNode(node: INavigable<any> | null): BlockSvg | null { getSourceBlockFromNode(node: IFocusableNode | null): BlockSvg | null {
if (node instanceof BlockSvg) { if (node instanceof BlockSvg) {
return node; return node;
} else if (node instanceof Field) { } else if (node instanceof Field) {

View File

@@ -4,9 +4,9 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
import type {INavigable} from '../interfaces/i_navigable.js'; import type {IFocusableNode} from '../interfaces/i_focusable_node.js';
import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js'; import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js';
import type {WorkspaceSvg} from '../workspace_svg.js'; import {WorkspaceSvg} from '../workspace_svg.js';
/** /**
* Set of rules controlling keyboard navigation from a workspace. * Set of rules controlling keyboard navigation from a workspace.
@@ -20,7 +20,7 @@ export class WorkspaceNavigationPolicy
* @param current The workspace to return the first child of. * @param current The workspace to return the first child of.
* @returns The top block of the first block stack, if any. * @returns The top block of the first block stack, if any.
*/ */
getFirstChild(current: WorkspaceSvg): INavigable<unknown> | null { getFirstChild(current: WorkspaceSvg): IFocusableNode | null {
const blocks = current.getTopBlocks(true); const blocks = current.getTopBlocks(true);
return blocks.length ? blocks[0] : null; return blocks.length ? blocks[0] : null;
} }
@@ -31,7 +31,7 @@ export class WorkspaceNavigationPolicy
* @param _current The workspace to return the parent of. * @param _current The workspace to return the parent of.
* @returns Null. * @returns Null.
*/ */
getParent(_current: WorkspaceSvg): INavigable<unknown> | null { getParent(_current: WorkspaceSvg): IFocusableNode | null {
return null; return null;
} }
@@ -41,7 +41,7 @@ export class WorkspaceNavigationPolicy
* @param _current The workspace to return the next sibling of. * @param _current The workspace to return the next sibling of.
* @returns Null. * @returns Null.
*/ */
getNextSibling(_current: WorkspaceSvg): INavigable<unknown> | null { getNextSibling(_current: WorkspaceSvg): IFocusableNode | null {
return null; return null;
} }
@@ -51,7 +51,7 @@ export class WorkspaceNavigationPolicy
* @param _current The workspace to return the previous sibling of. * @param _current The workspace to return the previous sibling of.
* @returns Null. * @returns Null.
*/ */
getPreviousSibling(_current: WorkspaceSvg): INavigable<unknown> | null { getPreviousSibling(_current: WorkspaceSvg): IFocusableNode | null {
return null; return null;
} }
@@ -64,4 +64,14 @@ export class WorkspaceNavigationPolicy
isNavigable(current: WorkspaceSvg): boolean { isNavigable(current: WorkspaceSvg): boolean {
return current.canBeFocused(); return current.canBeFocused();
} }
/**
* Returns whether the given object can be navigated from by this policy.
*
* @param current The object to check if this policy applies to.
* @returns True if the object is a WorkspaceSvg.
*/
isApplicable(current: any): current is WorkspaceSvg {
return current instanceof WorkspaceSvg;
}
} }

View File

@@ -4,10 +4,14 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
import type {INavigable} from './interfaces/i_navigable.js'; import type {IFocusableNode} from './interfaces/i_focusable_node.js';
import type {INavigationPolicy} from './interfaces/i_navigation_policy.js'; import type {INavigationPolicy} from './interfaces/i_navigation_policy.js';
import {BlockNavigationPolicy} from './keyboard_nav/block_navigation_policy.js';
import {ConnectionNavigationPolicy} from './keyboard_nav/connection_navigation_policy.js';
import {FieldNavigationPolicy} from './keyboard_nav/field_navigation_policy.js';
import {WorkspaceNavigationPolicy} from './keyboard_nav/workspace_navigation_policy.js';
type RuleMap<T> = Map<new (...args: any) => T, INavigationPolicy<T>>; type RuleList<T> = INavigationPolicy<T>[];
/** /**
* Class responsible for determining where focus should move in response to * Class responsible for determining where focus should move in response to
@@ -18,35 +22,35 @@ export class Navigator {
* Map from classes to a corresponding ruleset to handle navigation from * Map from classes to a corresponding ruleset to handle navigation from
* instances of that class. * instances of that class.
*/ */
private rules: RuleMap<any> = new Map(); protected rules: RuleList<any> = [
new BlockNavigationPolicy(),
new FieldNavigationPolicy(),
new ConnectionNavigationPolicy(),
new WorkspaceNavigationPolicy(),
];
/** /**
* Associates a navigation ruleset with its corresponding class. * Adds a navigation ruleset to this Navigator.
* *
* @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 * @param policy A ruleset that determines where focus should move starting
* from an instance of the given class. * from an instance of its managed class.
*/ */
set<T extends INavigable<T>>( addNavigationPolicy(policy: INavigationPolicy<any>) {
key: new (...args: any) => T, this.rules.push(policy);
policy: INavigationPolicy<T>,
) {
this.rules.set(key, policy);
} }
/** /**
* Returns the navigation ruleset associated with the given object instance's * Returns the navigation ruleset associated with the given object instance's
* class. * class.
* *
* @param key An object to retrieve a navigation ruleset for. * @param current An object to retrieve a navigation ruleset for.
* @returns The navigation ruleset of objects of the given object's class, or * @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. * undefined if no ruleset has been registered for the object's class.
*/ */
private get<T extends INavigable<T>>( private get(
key: T, current: IFocusableNode,
): INavigationPolicy<T> | undefined { ): INavigationPolicy<typeof current> | undefined {
return this.rules.get(key.getClass()); return this.rules.find((rule) => rule.isApplicable(current));
} }
/** /**
@@ -55,7 +59,7 @@ export class Navigator {
* @param current The object to retrieve the first child of. * @param current The object to retrieve the first child of.
* @returns The first child node of the given object, if any. * @returns The first child node of the given object, if any.
*/ */
getFirstChild<T extends INavigable<T>>(current: T): INavigable<any> | null { getFirstChild(current: IFocusableNode): IFocusableNode | null {
const result = this.get(current)?.getFirstChild(current); const result = this.get(current)?.getFirstChild(current);
if (!result) return null; if (!result) return null;
// If the child isn't navigable, don't traverse into it; check its peers. // If the child isn't navigable, don't traverse into it; check its peers.
@@ -71,7 +75,7 @@ export class Navigator {
* @param current The object to retrieve the parent of. * @param current The object to retrieve the parent of.
* @returns The parent node of the given object, if any. * @returns The parent node of the given object, if any.
*/ */
getParent<T extends INavigable<T>>(current: T): INavigable<any> | null { getParent(current: IFocusableNode): IFocusableNode | null {
const result = this.get(current)?.getParent(current); const result = this.get(current)?.getParent(current);
if (!result) return null; if (!result) return null;
if (!this.get(result)?.isNavigable(result)) return this.getParent(result); if (!this.get(result)?.isNavigable(result)) return this.getParent(result);
@@ -84,7 +88,7 @@ export class Navigator {
* @param current The object to retrieve the next sibling node of. * @param current The object to retrieve the next sibling node of.
* @returns The next sibling node of the given object, if any. * @returns The next sibling node of the given object, if any.
*/ */
getNextSibling<T extends INavigable<T>>(current: T): INavigable<any> | null { getNextSibling(current: IFocusableNode): IFocusableNode | null {
const result = this.get(current)?.getNextSibling(current); const result = this.get(current)?.getNextSibling(current);
if (!result) return null; if (!result) return null;
if (!this.get(result)?.isNavigable(result)) { if (!this.get(result)?.isNavigable(result)) {
@@ -99,9 +103,7 @@ export class Navigator {
* @param current The object to retrieve the previous sibling node of. * @param current The object to retrieve the previous sibling node of.
* @returns The previous sibling node of the given object, if any. * @returns The previous sibling node of the given object, if any.
*/ */
getPreviousSibling<T extends INavigable<T>>( getPreviousSibling(current: IFocusableNode): IFocusableNode | null {
current: T,
): INavigable<any> | null {
const result = this.get(current)?.getPreviousSibling(current); const result = this.get(current)?.getPreviousSibling(current);
if (!result) return null; if (!result) return null;
if (!this.get(result)?.isNavigable(result)) { if (!this.get(result)?.isNavigable(result)) {

View File

@@ -24,7 +24,6 @@ import {IContextMenu} from './interfaces/i_contextmenu.js';
import type {IFocusableNode} from './interfaces/i_focusable_node.js'; import type {IFocusableNode} from './interfaces/i_focusable_node.js';
import type {IFocusableTree} from './interfaces/i_focusable_tree.js'; import type {IFocusableTree} from './interfaces/i_focusable_tree.js';
import {hasBubble} from './interfaces/i_has_bubble.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 * as internalConstants from './internal_constants.js';
import {Coordinate} from './utils/coordinate.js'; import {Coordinate} from './utils/coordinate.js';
import * as svgMath from './utils/svg_math.js'; import * as svgMath from './utils/svg_math.js';
@@ -38,7 +37,7 @@ const BUMP_RANDOMNESS = 10;
*/ */
export class RenderedConnection export class RenderedConnection
extends Connection extends Connection
implements IContextMenu, IFocusableNode, INavigable<RenderedConnection> implements IContextMenu, IFocusableNode
{ {
// TODO(b/109816955): remove '!', see go/strict-prop-init-fix. // TODO(b/109816955): remove '!', see go/strict-prop-init-fix.
sourceBlock_!: BlockSvg; sourceBlock_!: BlockSvg;
@@ -664,15 +663,6 @@ export class RenderedConnection
| unknown | unknown
| null as SVGElement | null; | null as SVGElement | null;
} }
/**
* Returns this connection's class for keyboard navigation.
*
* @returns RenderedConnection.
*/
getClass() {
return RenderedConnection;
}
} }
export namespace RenderedConnection { export namespace RenderedConnection {

View File

@@ -53,7 +53,6 @@ import {
import type {IFocusableTree} from './interfaces/i_focusable_tree.js'; import type {IFocusableTree} from './interfaces/i_focusable_tree.js';
import {hasBubble} from './interfaces/i_has_bubble.js'; import {hasBubble} from './interfaces/i_has_bubble.js';
import type {IMetricsManager} from './interfaces/i_metrics_manager.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 {IToolbox} from './interfaces/i_toolbox.js';
import type {LineCursor} from './keyboard_nav/line_cursor.js'; import type {LineCursor} from './keyboard_nav/line_cursor.js';
import type {Marker} from './keyboard_nav/marker.js'; import type {Marker} from './keyboard_nav/marker.js';
@@ -100,11 +99,7 @@ const ZOOM_TO_FIT_MARGIN = 20;
*/ */
export class WorkspaceSvg export class WorkspaceSvg
extends Workspace extends Workspace
implements implements IContextMenu, IFocusableNode, IFocusableTree
IContextMenu,
IFocusableNode,
IFocusableTree,
INavigable<WorkspaceSvg>
{ {
/** /**
* A wrapper function called when a resize event occurs. * A wrapper function called when a resize event occurs.
@@ -2823,15 +2818,6 @@ export class WorkspaceSvg
} }
} }
/**
* Returns the class of this workspace.
*
* @returns WorkspaceSvg.
*/
getClass() {
return WorkspaceSvg;
}
/** /**
* Returns an object responsible for coordinating movement of focus between * Returns an object responsible for coordinating movement of focus between
* items on this workspace in response to keyboard navigation commands. * items on this workspace in response to keyboard navigation commands.
@@ -2841,6 +2827,16 @@ export class WorkspaceSvg
getNavigator(): Navigator { getNavigator(): Navigator {
return this.navigator; return this.navigator;
} }
/**
* Sets the Navigator instance used by this workspace.
*
* @param newNavigator A Navigator object to coordinate movement between
* elements on the workspace.
*/
setNavigator(newNavigator: Navigator) {
this.navigator = newNavigator;
}
} }
/** /**

View File

@@ -61,10 +61,6 @@ class FieldMitosis extends Field<CellGroup> {
}); });
this.value_ = {cells}; this.value_ = {cells};
} }
getClass() {
return FieldMitosis;
}
} }
fieldRegistry.register('field_mitosis', FieldMitosis); fieldRegistry.register('field_mitosis', FieldMitosis);