diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..80063ce2 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,22 @@ +# Build workflow +name: Build + +on: [pull_request, workflow_dispatch] + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: latest + cache: 'npm' + - run: npm ci + - run: npm run build + - run: npm run lint diff --git a/eslint.config.js b/eslint.config.js index d351d595..4b6119cb 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -129,7 +129,7 @@ module.exports = [ }, }, { - files: ['**/*.mocha.js'], + files: ['**/*.mocha.js', 'test/webdriverio/test/*_test.mjs'], languageOptions: { globals: { ...globals.mocha, diff --git a/src/actions/action_menu.ts b/src/actions/action_menu.ts index 584624d7..349da56f 100644 --- a/src/actions/action_menu.ts +++ b/src/actions/action_menu.ts @@ -10,7 +10,6 @@ import { ContextMenu, ContextMenuRegistry, ShortcutRegistry, - comments, utils as BlocklyUtils, WidgetDiv, } from 'blockly'; @@ -23,10 +22,7 @@ const createSerializedKey = ShortcutRegistry.registry.createSerializedKey.bind( ShortcutRegistry.registry, ); -export interface Scope { - block?: BlockSvg; - workspace?: WorkspaceSvg; - comment?: comments.RenderedWorkspaceComment; +export interface ScopeWithConnection extends ContextMenuRegistry.Scope { connection?: Connection; } @@ -100,6 +96,8 @@ export class ActionMenu { * Returns true if it is possible to open the action menu in the * current location, even if the menu was not opened due there being * no applicable menu items. + * + * @param workspace The workspace. */ private openActionMenu(workspace: WorkspaceSvg): boolean { let menuOptions: Array< @@ -114,7 +112,7 @@ export class ActionMenu { if (!node) return false; const nodeType = node.getType(); switch (nodeType) { - case ASTNode.types.BLOCK: + case ASTNode.types.BLOCK: { const block = node.getLocation() as BlockSvg; rtl = block.RTL; // Reimplement BlockSvg.prototype.generateContextMenu as that @@ -130,11 +128,12 @@ export class ActionMenu { } // End reimplement. break; + } // case Blockly.ASTNode.types.INPUT: case ASTNode.types.NEXT: case ASTNode.types.PREVIOUS: - case ASTNode.types.INPUT: + case ASTNode.types.INPUT: { const connection = node.getLocation() as Connection; rtl = connection.getSourceBlock().RTL; @@ -143,6 +142,7 @@ export class ActionMenu { // a possible kind of scope. this.addConnectionItems(connection, menuOptions); break; + } default: console.info(`No action menu for ASTNode of type ${nodeType}`); @@ -177,24 +177,21 @@ export class ActionMenu { */ private addConnectionItems( connection: Connection, - menuOptions: ( + menuOptions: Array< | ContextMenuRegistry.ContextMenuOption | ContextMenuRegistry.LegacyContextMenuOption - )[], + >, ) { - const insertAction = ContextMenuRegistry.registry.getItem('insert'); - if (!insertAction) throw new Error("can't find insert action"); - - const pasteAction = ContextMenuRegistry.registry.getItem( - 'blockPasteFromContextMenu', - ); - if (!pasteAction) throw new Error("can't find paste action"); - const possibleOptions = [insertAction, pasteAction /* etc.*/]; + const possibleOptions = [ + this.getContextMenuAction('insert'), + this.getContextMenuAction('blockPasteFromContextMenu'), + ]; // Check preconditions and get menu texts. const scope = { connection, } as unknown as ContextMenuRegistry.Scope; + for (const option of possibleOptions) { const precondition = option.preconditionFn?.(scope); if (precondition === 'hidden') continue; @@ -205,7 +202,7 @@ export class ActionMenu { menuOptions.push({ text: displayText, enabled: precondition === 'enabled', - callback: option.callback!, + callback: option.callback, scope, weight: option.weight, }); @@ -213,6 +210,25 @@ export class ActionMenu { return menuOptions; } + /** + * Find a context menu action, throwing an `Error` if it is not present or + * not an action. This usefully narrows the type to `ActionRegistryItem` + * which is not exported from Blockly. + * + * @param id The id of the action. + * @returns the action. + */ + private getContextMenuAction(id: string) { + const item = ContextMenuRegistry.registry.getItem(id); + if (!item) { + throw new Error(`can't find context menu item ${id}`); + } + if (!item?.callback) { + throw new Error(`context menu item unexpectedly not action ${id}`); + } + return item; + } + /** * Create a fake PointerEvent for opening the action menu for the * given ASTNode. diff --git a/src/actions/arrow_navigation.ts b/src/actions/arrow_navigation.ts index 7832c5ff..16cc3828 100644 --- a/src/actions/arrow_navigation.ts +++ b/src/actions/arrow_navigation.ts @@ -57,7 +57,7 @@ export class ArrowNavigation { right: { name: Constants.SHORTCUT_NAMES.RIGHT, preconditionFn: (workspace) => this.canCurrentlyNavigate(workspace), - callback: (workspace, _, shortcut) => { + callback: (workspace, e, shortcut) => { const toolbox = workspace.getToolbox() as Toolbox; let isHandled = false; switch (this.navigation.getState(workspace)) { @@ -94,7 +94,7 @@ export class ArrowNavigation { left: { name: Constants.SHORTCUT_NAMES.LEFT, preconditionFn: (workspace) => this.canCurrentlyNavigate(workspace), - callback: (workspace, _, shortcut) => { + callback: (workspace, e, shortcut) => { const toolbox = workspace.getToolbox() as Toolbox; let isHandled = false; switch (this.navigation.getState(workspace)) { @@ -129,7 +129,7 @@ export class ArrowNavigation { down: { name: Constants.SHORTCUT_NAMES.DOWN, preconditionFn: (workspace) => this.canCurrentlyNavigate(workspace), - callback: (workspace, _, shortcut) => { + callback: (workspace, e, shortcut) => { const toolbox = workspace.getToolbox() as Toolbox; const flyout = workspace.getFlyout(); let isHandled = false; @@ -170,7 +170,7 @@ export class ArrowNavigation { up: { name: Constants.SHORTCUT_NAMES.UP, preconditionFn: (workspace) => this.canCurrentlyNavigate(workspace), - callback: (workspace, _, shortcut) => { + callback: (workspace, e, shortcut) => { const flyout = workspace.getFlyout(); const toolbox = workspace.getToolbox() as Toolbox; let isHandled = false; diff --git a/src/actions/clipboard.ts b/src/actions/clipboard.ts index 46e8733d..6088c2af 100644 --- a/src/actions/clipboard.ts +++ b/src/actions/clipboard.ts @@ -18,6 +18,7 @@ import * as Constants from '../constants'; import type {BlockSvg, WorkspaceSvg} from 'blockly'; import {LineCursor} from '../line_cursor'; import {Navigation} from '../navigation'; +import {ScopeWithConnection} from './action_menu'; const KeyCodes = blocklyUtils.KeyCodes; const createSerializedKey = ShortcutRegistry.registry.createSerializedKey.bind( @@ -238,7 +239,7 @@ export class Clipboard { private copyPrecondition(workspace: WorkspaceSvg) { if (!this.canCurrentlyEdit(workspace)) return false; switch (this.navigation.getState(workspace)) { - case Constants.STATE.WORKSPACE: + case Constants.STATE.WORKSPACE: { const curNode = workspace?.getCursor()?.getCurNode(); const source = curNode?.getSourceBlock(); return !!( @@ -246,13 +247,15 @@ export class Clipboard { source?.isMovable() && !Gesture.inProgress() ); - case Constants.STATE.FLYOUT: + } + case Constants.STATE.FLYOUT: { const flyoutWorkspace = workspace.getFlyout()?.getWorkspace(); const sourceBlock = flyoutWorkspace ?.getCursor() ?.getCurNode() ?.getSourceBlock(); return !!(sourceBlock && !Gesture.inProgress()); + } default: return false; } @@ -314,18 +317,15 @@ export class Clipboard { private registerPasteContextMenuAction() { const pasteAction: ContextMenuRegistry.RegistryItem = { displayText: (scope) => `Paste (${this.getPlatformPrefix()}V)`, - preconditionFn: (scope) => { - const ws = - scope.block?.workspace ?? - (scope as any).connection?.getSourceBlock().workspace; + preconditionFn: (scope: ScopeWithConnection) => { + const block = scope.block ?? scope.connection?.getSourceBlock(); + const ws = block?.workspace as WorkspaceSvg | null; if (!ws) return 'hidden'; - return this.pastePrecondition(ws) ? 'enabled' : 'disabled'; }, - callback: (scope) => { - const ws = - scope.block?.workspace ?? - (scope as any).connection?.getSourceBlock().workspace; + callback: (scope: ScopeWithConnection) => { + const block = scope.block ?? scope.connection?.getSourceBlock(); + const ws = block?.workspace as WorkspaceSvg | null; if (!ws) return; return this.pasteCallback(ws); }, @@ -379,6 +379,7 @@ export class Clipboard { this.navigation.tryToConnectNodes( pasteWorkspace, targetNode, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion ASTNode.createBlockNode(block)!, ); } diff --git a/src/actions/delete.ts b/src/actions/delete.ts index 33498444..ba341e2a 100644 --- a/src/actions/delete.ts +++ b/src/actions/delete.ts @@ -113,7 +113,7 @@ export class DeleteAction { // Run the original precondition code, from the context menu option. // If the item would be hidden or disabled, respect it. const originalPreconditionResult = - this.oldContextMenuItem!.preconditionFn?.(scope) ?? 'enabled'; + this.oldContextMenuItem?.preconditionFn?.(scope) ?? 'enabled'; if (!ws || originalPreconditionResult !== 'enabled') { return originalPreconditionResult; } diff --git a/src/actions/disconnect.ts b/src/actions/disconnect.ts index 5da40fab..bbf91a7d 100644 --- a/src/actions/disconnect.ts +++ b/src/actions/disconnect.ts @@ -117,12 +117,17 @@ export class DisconnectAction { if (!curConnection.isConnected()) { return; } + const targetConnection = curConnection.targetConnection; + if (!targetConnection) { + throw new Error('Must have target if connected'); + } + const superiorConnection = curConnection.isSuperior() ? curConnection - : curConnection.targetConnection!; + : targetConnection; const inferiorConnection = curConnection.isSuperior() - ? curConnection.targetConnection! + ? targetConnection : curConnection; if (inferiorConnection.getSourceBlock().isShadow()) { @@ -141,7 +146,7 @@ export class DisconnectAction { if (wasVisitingConnection) { const connectionNode = ASTNode.createConnectionNode(superiorConnection); - workspace.getCursor()!.setCurNode(connectionNode!); + workspace.getCursor()?.setCurNode(connectionNode); } } } diff --git a/src/actions/edit.ts b/src/actions/edit.ts index 9d6f7254..be997fdf 100644 --- a/src/actions/edit.ts +++ b/src/actions/edit.ts @@ -4,19 +4,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { - Connection, - ContextMenuRegistry, - ShortcutRegistry, - comments, - utils as BlocklyUtils, -} from 'blockly'; -import * as Constants from '../constants'; -import type {BlockSvg, WorkspaceSvg} from 'blockly'; +import {ContextMenuRegistry} from 'blockly'; +import type {WorkspaceSvg} from 'blockly'; import {LineCursor} from '../line_cursor'; -import {NavigationController} from '../navigation_controller'; - -const KeyCodes = BlocklyUtils.KeyCodes; /** * Action to edit a block. This just moves the cursor to the first diff --git a/src/actions/enter.ts b/src/actions/enter.ts index c168a627..9071fa54 100644 --- a/src/actions/enter.ts +++ b/src/actions/enter.ts @@ -134,6 +134,7 @@ export class EnterAction { !this.navigation.tryToConnectNodes( workspace, stationaryNode, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion ASTNode.createBlockNode(newBlock)!, ) ) { @@ -144,7 +145,7 @@ export class EnterAction { } this.navigation.focusWorkspace(workspace); - workspace.getCursor()!.setCurNode(ASTNode.createBlockNode(newBlock)!); + workspace.getCursor()?.setCurNode(ASTNode.createBlockNode(newBlock)); } /** @@ -155,17 +156,22 @@ export class EnterAction { */ private triggerButtonCallback(workspace: WorkspaceSvg) { const button = this.navigation - .getFlyoutCursor(workspace)! - .getCurNode() + .getFlyoutCursor(workspace) + ?.getCurNode() ?.getLocation() as FlyoutButton | undefined; if (!button) return; - const buttonCallback = (workspace as any).flyoutButtonCallbacks.get( - (button as any).callbackKey, - ); - if (typeof buttonCallback === 'function') { + + const flyoutButtonCallbacks: Map void> = + // @ts-expect-error private field access + workspace.flyoutButtonCallbacks; + + const info = button.info; + if ('callbackkey' in info) { + const buttonCallback = flyoutButtonCallbacks.get(info.callbackkey); + if (!buttonCallback) { + throw new Error('No callback function found for flyout button.'); + } buttonCallback(button); - } else if (!button.isLabel()) { - throw new Error('No callback function found for flyout button.'); } } @@ -208,8 +214,8 @@ export class EnterAction { } const curBlock = this.navigation - .getFlyoutCursor(workspace)! - .getCurNode() + .getFlyoutCursor(workspace) + ?.getCurNode() ?.getLocation() as BlockSvg | undefined; if (!curBlock?.isEnabled()) { console.warn("Can't insert a disabled block."); diff --git a/src/actions/insert.ts b/src/actions/insert.ts index 0f8f4a42..84c60117 100644 --- a/src/actions/insert.ts +++ b/src/actions/insert.ts @@ -5,16 +5,14 @@ */ import { - Connection, ContextMenuRegistry, ShortcutRegistry, - comments, utils as BlocklyUtils, } from 'blockly'; import * as Constants from '../constants'; -import type {BlockSvg, WorkspaceSvg} from 'blockly'; +import type {WorkspaceSvg} from 'blockly'; import {Navigation} from '../navigation'; -import {Scope} from './action_menu'; +import {ScopeWithConnection} from './action_menu'; const KeyCodes = BlocklyUtils.KeyCodes; @@ -87,15 +85,15 @@ export class InsertAction { return 'Insert Block (I)'; } }, - preconditionFn: (scope: Scope) => { + preconditionFn: (scope: ScopeWithConnection) => { const block = scope.block ?? scope.connection?.getSourceBlock(); const ws = block?.workspace as WorkspaceSvg | null; if (!ws) return 'hidden'; return this.insertPrecondition(ws) ? 'enabled' : 'hidden'; }, - callback: (scope: Scope) => { - let ws = + callback: (scope: ScopeWithConnection) => { + const ws = scope.block?.workspace ?? (scope.connection?.getSourceBlock().workspace as WorkspaceSvg); if (!ws) return false; diff --git a/src/actions/ws_movement.ts b/src/actions/ws_movement.ts index 25700c9d..048a0b90 100644 --- a/src/actions/ws_movement.ts +++ b/src/actions/ws_movement.ts @@ -13,6 +13,11 @@ const createSerializedKey = ShortcutRegistry.registry.createSerializedKey.bind( ShortcutRegistry.registry, ); +/** + * The distance to move the cursor when the cursor is on the workspace. + */ +const WS_MOVE_DISTANCE = 40; + /** * Logic for free movement of the cursor on the workspace with keyboard * shortcuts. @@ -24,11 +29,6 @@ export class WorkspaceMovement { */ private canCurrentlyEdit: (ws: WorkspaceSvg) => boolean; - /** - * The distance to move the cursor when the cursor is on the workspace. - */ - WS_MOVE_DISTANCE = 40; - constructor(canEdit: (ws: WorkspaceSvg) => boolean) { this.canCurrentlyEdit = canEdit; } @@ -111,14 +111,14 @@ export class WorkspaceMovement { if (!curNode || curNode.getType() !== ASTNode.types.WORKSPACE) return false; const wsCoord = curNode.getWsCoordinate(); - const newX = xDirection * this.WS_MOVE_DISTANCE + wsCoord.x; - const newY = yDirection * this.WS_MOVE_DISTANCE + wsCoord.y; + const newX = xDirection * WS_MOVE_DISTANCE + wsCoord.x; + const newY = yDirection * WS_MOVE_DISTANCE + wsCoord.y; cursor.setCurNode( ASTNode.createWorkspaceNode( workspace, new BlocklyUtils.Coordinate(newX, newY), - )!, + ), ); return true; } diff --git a/src/constants.ts b/src/constants.ts index 4fb5cedd..5884cabf 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,3 +1,6 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +// The rules expect camel or pascal case enum members and record properties. + /** * @license * Copyright 2021 Google LLC @@ -38,13 +41,11 @@ export enum SHORTCUT_NAMES { CUT = 'keyboard_nav_cut', PASTE = 'keyboard_nav_paste', DELETE = 'keyboard_nav_delete', - /* eslint-disable @typescript-eslint/naming-convention */ MOVE_WS_CURSOR_UP = 'workspace_up', MOVE_WS_CURSOR_DOWN = 'workspace_down', MOVE_WS_CURSOR_LEFT = 'workspace_left', MOVE_WS_CURSOR_RIGHT = 'workspace_right', CREATE_WS_CURSOR = 'to_workspace', - /* eslint-enable @typescript-eslint/naming-convention */ LIST_SHORTCUTS = 'list_shortcuts', CLEAN_UP = 'clean_up_workspace', } diff --git a/src/flyout_cursor.ts b/src/flyout_cursor.ts index e4abdf10..1ca7bbb1 100644 --- a/src/flyout_cursor.ts +++ b/src/flyout_cursor.ts @@ -20,6 +20,8 @@ import {scrollBoundsIntoView} from './workspace_utilities'; export class FlyoutCursor extends Blockly.Cursor { /** * The constructor for the FlyoutCursor. + * + * @param flyout The flyout this cursor is for. */ constructor(private readonly flyout: Blockly.IFlyout) { super(); @@ -81,12 +83,13 @@ export class FlyoutCursor extends Blockly.Cursor { return null; } - override setCurNode(node: Blockly.ASTNode) { + override setCurNode(node: Blockly.ASTNode | null) { super.setCurNode(node); - const location = node.getLocation(); + const location = node?.getLocation(); let bounds: Blockly.utils.Rect | undefined; if ( + location && 'getBoundingRectangle' in location && typeof location.getBoundingRectangle === 'function' ) { diff --git a/src/index.ts b/src/index.ts index 63efa4e0..e0773444 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,9 +10,9 @@ import {CursorOptions, LineCursor} from './line_cursor'; import {getFlyoutElement, getToolboxElement} from './workspace_utilities'; /** Options object for KeyboardNavigation instances. */ -export type NavigationOptions = { +export interface NavigationOptions { cursor: Partial; -}; +} /** Default options for LineCursor instances. */ const defaultOptions: NavigationOptions = { @@ -73,6 +73,7 @@ export class KeyboardNavigation { * * @param workspace The workspace that the plugin will * be added to. + * @param options Options. */ constructor( workspace: Blockly.WorkspaceSvg, diff --git a/src/keynames.js b/src/keynames.ts similarity index 78% rename from src/keynames.js rename to src/keynames.ts index d2e78bfa..f8c5b0d6 100644 --- a/src/keynames.js +++ b/src/keynames.ts @@ -19,7 +19,8 @@ * * Copied from goog.events.keynames */ -const keyNames = { +const keyNames: Record = { + /* eslint-disable @typescript-eslint/naming-convention */ 8: 'backspace', 9: 'tab', 13: 'enter', @@ -118,6 +119,7 @@ const keyNames = { 221: 'close-square-bracket', 222: 'single-quote', 224: 'win', + /* eslint-enable @typescript-eslint/naming-convention */ }; const modifierKeys = ['control', 'alt', 'meta']; @@ -126,12 +128,20 @@ const modifierKeys = ['control', 'alt', 'meta']; * Assign the appropriate class names for the key. * Modifier keys are indicated so they can be switched to a platform specific * key. + * + * @param keyName The key name. */ -function getKeyClassName(keyName) { +function getKeyClassName(keyName: string) { return modifierKeys.includes(keyName.toLowerCase()) ? 'key modifier' : 'key'; } -export function toTitleCase(str) { +/** + * Naive title case conversion. Uppercases first and lowercases remainder. + * + * @param str String. + * @returns The string in title case. + */ +export function toTitleCase(str: string) { return str.charAt(0).toUpperCase() + str.substring(1).toLowerCase(); } @@ -139,11 +149,13 @@ export function toTitleCase(str) { * Convert from a serialized key code to a HTML string. * This should be the inverse of ShortcutRegistry.createSerializedKey, but * should also convert ascii characters to strings. - * @param {string} keycode The key code as a string of characters separated + * + * @param keycode The key code as a string of characters separated * by the + character. - * @returns {string} A single string representing the key code. + * @param index Which key code this is in sequence. + * @returns A single string representing the key code. */ -function keyCodeToString(keycode, index) { +function keyCodeToString(keycode: string, index: number) { let result = ``; const pieces = keycode.split('+'); @@ -154,29 +166,23 @@ function keyCodeToString(keycode, index) { piece = pieces[i]; strrep = keyNames[piece] ?? piece; const className = getKeyClassName(strrep); - - if (i.length === 1) { - strrep = strrep.toUpperCase(); - } else { - strrep = toTitleCase(strrep); - } - if (i > 0) { result += '+'; } - result += `${strrep}`; + result += `${toTitleCase(strrep)}`; } result += ''; return result; } /** - * Convert an array of key codes into a comma-separated list of strings - * @param {Array} keycodeArr The array of key codes to convert. - * @returns {string} The input array as a comma-separated list of + * Convert an array of key codes into a comma-separated list of strings. + * + * @param keycodeArr The array of key codes to convert. + * @returns The input array as a comma-separated list of * human-readable strings wrapped in HTML. */ -export function keyCodeArrayToString(keycodeArr) { +export function keyCodeArrayToString(keycodeArr: string[]): string { const stringified = keycodeArr.map((keycode, index) => keyCodeToString(keycode, index), ); diff --git a/src/line_cursor.ts b/src/line_cursor.ts index 54508590..b810ddf9 100644 --- a/src/line_cursor.ts +++ b/src/line_cursor.ts @@ -18,13 +18,13 @@ import {ASTNode, Marker} from 'blockly/core'; import {scrollBoundsIntoView} from './workspace_utilities'; /** Options object for LineCursor instances. */ -export type CursorOptions = { +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 = { @@ -50,13 +50,14 @@ export class LineCursor extends Marker { private potentialNodes: Blockly.ASTNode[] | null = null; /** Whether the renderer is zelos-style. */ - private isZelos: boolean = false; + private isZelos = false; /** * @param workspace The workspace this cursor belongs to. + * @param options Cursor options. */ constructor( - public readonly workspace: Blockly.WorkspaceSvg, + private readonly workspace: Blockly.WorkspaceSvg, options?: Partial, ) { super(); @@ -111,7 +112,7 @@ export class LineCursor extends Marker { if (!curNode) { return null; } - let newNode = this.getNextNode(curNode, this.validLineNode.bind(this)); + const newNode = this.getNextNode(curNode, this.validLineNode.bind(this)); if (newNode) { this.setCurNode(newNode); @@ -150,7 +151,10 @@ export class LineCursor extends Marker { if (!curNode) { return null; } - let newNode = this.getPreviousNode(curNode, this.validLineNode.bind(this)); + const newNode = this.getPreviousNode( + curNode, + this.validLineNode.bind(this), + ); if (newNode) { this.setCurNode(newNode); @@ -187,7 +191,7 @@ export class LineCursor extends Marker { * - in effect, if the LineCursor is at the end of the 'current * line' of the program. */ - public atEndOfLine(): boolean { + atEndOfLine(): boolean { const curNode = this.getCurNode(); if (!curNode) return false; const rightNode = this.getNextNode( @@ -227,12 +231,13 @@ export class LineCursor extends Marker { switch (type) { case ASTNode.types.BLOCK: return !(location as Blockly.Block).outputConnection?.isConnected(); - case ASTNode.types.INPUT: + case ASTNode.types.INPUT: { const connection = location as Blockly.Connection; return ( connection.type === Blockly.NEXT_STATEMENT && (this.options.stackConnections || !connection.isConnected()) ); + } case ASTNode.types.NEXT: return ( this.options.stackConnections || @@ -388,13 +393,17 @@ 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 | null): ASTNode | null { - if (!node!.in()) { + private getRightMostChild(node: ASTNode): ASTNode | null { + let newNode = node.in(); + if (!newNode) { return node; } - let newNode = node!.in(); - while (newNode && newNode.next()) { - newNode = newNode.next(); + for ( + let nextNode: ASTNode | null = newNode; + nextNode; + nextNode = newNode.next() + ) { + newNode = nextNode; } return this.getRightMostChild(newNode); } @@ -573,6 +582,7 @@ export class LineCursor extends Marker { * * @param oldNode The previous node. * @param curNode The current node. + * @param realDrawer The object ~in charge of drawing the marker. */ private drawMarker( oldNode: ASTNode | null, @@ -620,7 +630,8 @@ export class LineCursor extends Marker { // Call MarkerSvg.prototype.fireMarkerEvent like // MarkerSvg.prototype.draw would (even though it's private). - (realDrawer as any)?.fireMarkerEvent?.(oldNode, curNode); + // @ts-expect-error calling protected method + realDrawer?.fireMarkerEvent?.(oldNode, curNode); } /** @@ -733,7 +744,7 @@ export class LineCursor extends Marker { block = block.getParent(); } if (block) { - this.setCurNode(Blockly.ASTNode.createBlockNode(block)!, true); + this.setCurNode(Blockly.ASTNode.createBlockNode(block), true); } } } diff --git a/src/navigation.ts b/src/navigation.ts index fdd115fe..29cf84cb 100644 --- a/src/navigation.ts +++ b/src/navigation.ts @@ -24,6 +24,22 @@ import { } from './workspace_utilities'; import {PassiveFocus} from './passive_focus'; +/** + * The default coordinate to use when focusing on the workspace and no + * blocks are present. In pixel coordinates, but will be converted to + * workspace coordinates when used to position the cursor. + */ +const DEFAULT_WS_COORDINATE: Blockly.utils.Coordinate = + new Blockly.utils.Coordinate(100, 100); + +/** + * The default coordinate to use when moving the cursor to the workspace + * after a block has been deleted. In pixel coordinates, but will be + * converted to workspace coordinates when used to position the cursor. + */ +const WS_COORDINATE_ON_DELETE: Blockly.utils.Coordinate = + new Blockly.utils.Coordinate(100, 100); + /** * Class that holds all methods necessary for keyboard navigation to work. */ @@ -34,22 +50,6 @@ export class Navigation { */ workspaceStates: {[index: string]: Constants.STATE} = {}; - /** - * The default coordinate to use when focusing on the workspace and no - * blocks are present. In pixel coordinates, but will be converted to - * workspace coordinates when used to position the cursor. - */ - DEFAULT_WS_COORDINATE: Blockly.utils.Coordinate = - new Blockly.utils.Coordinate(100, 100); - - /** - * The default coordinate to use when moving the cursor to the workspace - * after a block has been deleted. In pixel coordinates, but will be - * converted to workspace coordinates when used to position the cursor. - */ - WS_COORDINATE_ON_DELETE: Blockly.utils.Coordinate = - new Blockly.utils.Coordinate(100, 100); - /** * Wrapper for method that deals with workspace changes. * Used for removing change listener. @@ -263,15 +263,21 @@ export class Navigation { e.type === Blockly.Events.CLICK && (e as Blockly.Events.Click).targetType === 'block' ) { - const block = flyoutWorkspace.getBlockById( - (e as Blockly.Events.Click).blockId!, - ); - this.handleBlockClickInFlyout(mainWorkspace, block!); + const {blockId} = e as Blockly.Events.Click; + if (blockId) { + const block = flyoutWorkspace.getBlockById(blockId); + if (block) { + this.handleBlockClickInFlyout(mainWorkspace, block); + } + } } else if (e.type === Blockly.Events.SELECTED) { - const block = flyoutWorkspace.getBlockById( - (e as Blockly.Events.Selected).newElementId!, - ); - this.handleBlockClickInFlyout(mainWorkspace, block!); + const {newElementId} = e as Blockly.Events.Selected; + if (newElementId) { + const block = flyoutWorkspace.getBlockById(newElementId); + if (block) { + this.handleBlockClickInFlyout(mainWorkspace, block); + } + } } } else if ( e.type === Blockly.Events.BLOCK_CREATE && @@ -314,7 +320,7 @@ export class Navigation { const curNode = cursor.getCurNode(); const block = curNode ? curNode.getSourceBlock() : null; if (block && block.id === mutatedBlockId) { - cursor.setCurNode(Blockly.ASTNode.createBlockNode(block)!); + cursor.setCurNode(Blockly.ASTNode.createBlockNode(block)); } } } @@ -333,24 +339,15 @@ export class Navigation { const deletedBlockId = e.blockId; const ids = e.ids ?? []; const cursor = workspace.getCursor(); + if (!cursor) return; // Make sure the cursor is on a block. - if ( - !cursor || - !cursor.getCurNode() || - !cursor.getCurNode()?.getSourceBlock() - ) { - return; - } + const sourceBlock = cursor.getCurNode()?.getSourceBlock(); + if (!sourceBlock) return; - const curNode = cursor.getCurNode(); - const sourceBlock = curNode?.getSourceBlock()!; - if (sourceBlock?.id === deletedBlockId || ids.includes(sourceBlock?.id)) { + if (sourceBlock.id === deletedBlockId || ids.includes(sourceBlock.id)) { cursor.setCurNode( - Blockly.ASTNode.createWorkspaceNode( - workspace, - this.WS_COORDINATE_ON_DELETE, - )!, + Blockly.ASTNode.createWorkspaceNode(workspace, WS_COORDINATE_ON_DELETE), ); } } @@ -369,12 +366,12 @@ export class Navigation { if (!block) { return; } - if (block.isShadow()) { - block = block.getParent()!; + const curNodeBlock = block.isShadow() ? block : block.getParent(); + if (curNodeBlock) { + this.getFlyoutCursor(mainWorkspace)?.setCurNode( + Blockly.ASTNode.createStackNode(curNodeBlock), + ); } - this.getFlyoutCursor(mainWorkspace)!.setCurNode( - Blockly.ASTNode.createStackNode(block)!, - ); this.focusFlyout(mainWorkspace); } @@ -488,9 +485,9 @@ export class Navigation { } this.setState(workspace, Constants.STATE.TOOLBOX); - if (!toolbox.getSelectedItem()) { + if (!toolbox.getSelectedItem() && toolbox instanceof Blockly.Toolbox) { // Find the first item that is selectable. - const toolboxItems = (toolbox as any).getToolboxItems(); + const toolboxItems = toolbox.getToolboxItems(); for (let i = 0, toolboxItem; (toolboxItem = toolboxItems[i]); i++) { if (toolboxItem.isSelectable()) { toolbox.selectItemByPosition(i); @@ -603,13 +600,13 @@ export class Navigation { const astNode = Blockly.ASTNode.createButtonNode( defaultFlyoutItemElement as Blockly.FlyoutButton, ); - flyoutCursor.setCurNode(astNode!); + flyoutCursor.setCurNode(astNode); return true; } else if (defaultFlyoutItemElement instanceof Blockly.BlockSvg) { const astNode = Blockly.ASTNode.createStackNode( defaultFlyoutItemElement as Blockly.BlockSvg, ); - flyoutCursor.setCurNode(astNode!); + flyoutCursor.setCurNode(astNode); return true; } return false; @@ -642,21 +639,21 @@ export class Navigation { return false; } const wsCoordinates = new Blockly.utils.Coordinate( - this.DEFAULT_WS_COORDINATE.x / workspace.scale, - this.DEFAULT_WS_COORDINATE.y / workspace.scale, + DEFAULT_WS_COORDINATE.x / workspace.scale, + DEFAULT_WS_COORDINATE.y / workspace.scale, ); if (topBlocks.length > 0) { cursor.setCurNode( Blockly.ASTNode.createTopNode( topBlocks[prefer === 'first' ? 0 : topBlocks.length - 1], - )!, + ), ); } else { const wsNode = Blockly.ASTNode.createWorkspaceNode( workspace, wsCoordinates, ); - cursor.setCurNode(wsNode!); + cursor.setCurNode(wsNode); } return true; } @@ -1077,9 +1074,9 @@ export class Navigation { workspace.keyboardAccessibilityMode ) { workspace.keyboardAccessibilityMode = false; - workspace.getCursor()!.hide(); + workspace.getCursor()?.hide(); if (this.getFlyoutCursor(workspace)) { - this.getFlyoutCursor(workspace)!.hide(); + this.getFlyoutCursor(workspace)?.hide(); } } } @@ -1117,6 +1114,7 @@ export class Navigation { /** * Save the current cursor location and open the toolbox or flyout * to select and insert a block. + * * @param workspace The active workspace. */ openToolboxOrFlyout(workspace: Blockly.WorkspaceSvg) { @@ -1149,6 +1147,7 @@ export class Navigation { this.tryToConnectNodes( workspace, targetNode, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion Blockly.ASTNode.createBlockNode(block)!, ); } diff --git a/src/navigation_controller.ts b/src/navigation_controller.ts index 9d9d2c62..2dc62479 100644 --- a/src/navigation_controller.ts +++ b/src/navigation_controller.ts @@ -149,13 +149,17 @@ export class NavigationController { } switch (shortcut.name) { case Constants.SHORTCUT_NAMES.UP: - return (this as any).selectPrevious(); + // @ts-expect-error private method + return this.selectPrevious(); case Constants.SHORTCUT_NAMES.LEFT: - return (this as any).selectParent(); + // @ts-expect-error private method + return this.selectParent(); case Constants.SHORTCUT_NAMES.DOWN: - return (this as any).selectNext(); + // @ts-expect-error private method + return this.selectNext(); case Constants.SHORTCUT_NAMES.RIGHT: - return (this as any).selectChild(); + // @ts-expect-error private method + return this.selectChild(); default: return false; } diff --git a/src/passive_focus.ts b/src/passive_focus.ts index 17ebbff5..08719211 100644 --- a/src/passive_focus.ts +++ b/src/passive_focus.ts @@ -74,6 +74,8 @@ export class PassiveFocus { /** * Show the passive focus indicator at the specified location. * Implementation varies based on location type. + * + * @param node The node to show passive focus for. */ show(node: ASTNode) { // Hide last shown. diff --git a/src/shortcut_dialog.ts b/src/shortcut_dialog.ts index 3883640c..80078b82 100644 --- a/src/shortcut_dialog.ts +++ b/src/shortcut_dialog.ts @@ -7,7 +7,6 @@ import * as Blockly from 'blockly/core'; import * as Constants from './constants'; import {ShortcutRegistry} from 'blockly/core'; -// @ts-expect-error No types in js file import {keyCodeArrayToString, toTitleCase} from './keynames'; /** @@ -88,12 +87,13 @@ export class ShortcutDialog { } /** - * @param {string} shortcutName Shortcut name to convert. - * @returns {string} + * Munges a shortcut name into human readable text. + * + * @param shortcutName Shortcut name to convert. + * @returns A title case version of the name. */ getReadableShortcutName(shortcutName: string) { - shortcutName = toTitleCase(shortcutName.replace(/_/gi, ' ')); - return shortcutName; + return toTitleCase(shortcutName.replace(/_/gi, ' ')); } /** @@ -188,7 +188,7 @@ export class ShortcutDialog { /** * Register classes used by the shortcuts modal * Alt: plugin exports a register() function that updates the registry - **/ + */ Blockly.Css.register(` :root { --divider-border-color: #eee; diff --git a/test/blocks/p5_blocks.js b/test/blocks/p5_blocks.js index 9e2d5e1c..83654f74 100644 --- a/test/blocks/p5_blocks.js +++ b/test/blocks/p5_blocks.js @@ -1,3 +1,4 @@ +/* eslint-disable camelcase */ /** * @license * Copyright 2024 Google LLC diff --git a/test/blocks/p5_generators.js b/test/blocks/p5_generators.js index a33660b4..f642dc88 100644 --- a/test/blocks/p5_generators.js +++ b/test/blocks/p5_generators.js @@ -1,3 +1,4 @@ +/* eslint-disable camelcase */ /** * @license * Copyright 2023 Google LLC diff --git a/test/index.ts b/test/index.ts index 80784c1d..7af7ca27 100644 --- a/test/index.ts +++ b/test/index.ts @@ -6,7 +6,7 @@ import * as Blockly from 'blockly'; // Import the default blocks. -import * as libraryBlocks from 'blockly/blocks'; +import 'blockly/blocks'; import {installAllBlocks as installColourBlocks} from '@blockly/field-colour'; import {KeyboardNavigation} from '../src/index'; // @ts-expect-error No types in js file @@ -89,7 +89,10 @@ function createWorkspace(): Blockly.WorkspaceSvg { toolbox, renderer, }; - const blocklyDiv = document.getElementById('blocklyDiv')!; + const blocklyDiv = document.getElementById('blocklyDiv'); + if (!blocklyDiv) { + throw new Error('Missing blocklyDiv'); + } const workspace = Blockly.inject(blocklyDiv, injectOptions); const navigationOptions = { diff --git a/test/webdriverio/index.ts b/test/webdriverio/index.ts index c3078e2c..d9999790 100644 --- a/test/webdriverio/index.ts +++ b/test/webdriverio/index.ts @@ -6,7 +6,7 @@ import * as Blockly from 'blockly'; // Import the default blocks. -import * as libraryBlocks from 'blockly/blocks'; +import 'blockly/blocks'; import {installAllBlocks as installColourBlocks} from '@blockly/field-colour'; import {KeyboardNavigation} from '../../src/index'; // @ts-expect-error No types in js file @@ -73,7 +73,10 @@ function createWorkspace(): Blockly.WorkspaceSvg { toolbox, renderer, }; - const blocklyDiv = document.getElementById('blocklyDiv')!; + const blocklyDiv = document.getElementById('blocklyDiv'); + if (!blocklyDiv) { + throw new Error('Missing blocklyDiv'); + } const workspace = Blockly.inject(blocklyDiv, injectOptions); const navigationOptions = { @@ -105,6 +108,7 @@ document.addEventListener('DOMContentLoaded', () => { createWorkspace(); // Add Blockly to the global scope so that test code can access it to // verify state after keypresses. - // @ts-ignore + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error window.Blockly = Blockly; }); diff --git a/test/webdriverio/test/test_setup.mjs b/test/webdriverio/test/test_setup.mjs index f3b54c20..b4c59f40 100644 --- a/test/webdriverio/test/test_setup.mjs +++ b/test/webdriverio/test/test_setup.mjs @@ -40,7 +40,7 @@ export const PAUSE_TIME = 50; /** * Start up the test page. This should only be done once, to avoid * constantly popping browser windows open and closed. - * @return A Promise that resolves to a webdriverIO browser that tests can manipulate. + * @returns {Promise} A Promise that resolves to a webdriverIO browser that tests can manipulate. */ export async function driverSetup() { const options = { @@ -90,7 +90,7 @@ export async function driverTeardown() { * Navigate to the correct URL for the test, using the shared driver. * @param {string} playgroundUrl The URL to open for the test, which should be * a Blockly playground with a workspace. - * @return A Promsie that resolves to a webdriverIO browser that tests can manipulate. + * @returns {Promise} A Promsie that resolves to a webdriverIO browser that tests can manipulate. */ export async function testSetup(playgroundUrl) { if (!driver) { @@ -106,11 +106,11 @@ export async function testSetup(playgroundUrl) { /** * Replaces OS-specific path with POSIX style path. + * * Simplified implementation based on * https://stackoverflow.com/a/63251716/4969945 - * * @param {string} target target path - * @return {string} posix path + * @returns {string} posix path */ function posixPath(target) { const result = target.split(path.sep).join(path.posix.sep);