UNPKG

@blockly/keyboard-navigation

Version:
244 lines (229 loc) 7.88 kB
/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import { BlockSvg, ContextMenuRegistry, Msg, ShortcutRegistry, utils, WorkspaceSvg, keyboardNavigationController, getFocusManager, } from 'blockly'; import {Direction} from '../drag_direction'; import {Mover} from './mover'; import {getShortActionShortcut} from '../shortcut_formatting'; const KeyCodes = utils.KeyCodes; const createSerializedKey = ShortcutRegistry.registry.createSerializedKey.bind( ShortcutRegistry.registry, ); /** * Actions for moving blocks with keyboard shortcuts. */ export class MoveActions { constructor(private mover: Mover) {} private shortcutNames: string[] = []; private menuItemNames: string[] = []; private registerShortcuts() { const shortcuts: ShortcutRegistry.KeyboardShortcut[] = [ // Begin and end move. { name: Msg['START_MOVE'], preconditionFn: (workspace) => { const startBlock = this.getCurrentBlock(workspace); return !!startBlock && this.mover.canMove(workspace, startBlock); }, callback: (workspace) => { keyboardNavigationController.setIsActive(true); const startBlock = this.getCurrentBlock(workspace); // Focus the start block in case one of its fields or a shadow block // was focused when the move was triggered. if (startBlock) { getFocusManager().focusNode(startBlock); } return ( !!startBlock && this.mover.startMove(workspace, startBlock, null) ); }, keyCodes: [KeyCodes.M], }, { name: Msg['FINISH_MOVE'], preconditionFn: (workspace) => this.mover.isMoving(workspace), callback: (workspace) => this.mover.finishMove(workspace), keyCodes: [KeyCodes.ENTER, KeyCodes.SPACE], allowCollision: true, }, { name: Msg['ABORT_MOVE'], preconditionFn: (workspace) => this.mover.isMoving(workspace), callback: (workspace) => this.mover.abortMove(workspace), keyCodes: [KeyCodes.ESC], allowCollision: true, }, // Constrained moves. { name: Msg['MOVE_LEFT_CONSTRAINED'], preconditionFn: (workspace) => this.mover.isMoving(workspace), callback: (workspace) => this.mover.moveConstrained(workspace, Direction.Left), keyCodes: [KeyCodes.LEFT], allowCollision: true, }, { name: Msg['MOVE_RIGHT_CONSTRAINED'], preconditionFn: (workspace) => this.mover.isMoving(workspace), callback: (workspace) => this.mover.moveConstrained(workspace, Direction.Right), keyCodes: [KeyCodes.RIGHT], allowCollision: true, }, { name: Msg['MOVE_UP_CONSTRAINED'], preconditionFn: (workspace) => this.mover.isMoving(workspace), callback: (workspace) => this.mover.moveConstrained(workspace, Direction.Up), keyCodes: [KeyCodes.UP], allowCollision: true, }, { name: Msg['MOVE_DOWN_CONSTRAINED'], preconditionFn: (workspace) => this.mover.isMoving(workspace), callback: (workspace) => this.mover.moveConstrained(workspace, Direction.Down), keyCodes: [KeyCodes.DOWN], allowCollision: true, }, // Unconstrained moves. { name: Msg['MOVE_LEFT_UNCONSTRAINED'], preconditionFn: (workspace) => this.mover.isMoving(workspace), callback: (workspace) => this.mover.moveUnconstrained(workspace, Direction.Left), keyCodes: [ createSerializedKey(KeyCodes.LEFT, [KeyCodes.ALT]), createSerializedKey(KeyCodes.LEFT, [KeyCodes.CTRL]), ], }, { name: Msg['MOVE_RIGHT_UNCONSTRAINED'], preconditionFn: (workspace) => this.mover.isMoving(workspace), callback: (workspace) => this.mover.moveUnconstrained(workspace, Direction.Right), keyCodes: [ createSerializedKey(KeyCodes.RIGHT, [KeyCodes.ALT]), createSerializedKey(KeyCodes.RIGHT, [KeyCodes.CTRL]), ], }, { name: Msg['MOVE_UP_UNCONSTRAINED'], preconditionFn: (workspace) => this.mover.isMoving(workspace), callback: (workspace) => this.mover.moveUnconstrained(workspace, Direction.Up), keyCodes: [ createSerializedKey(KeyCodes.UP, [KeyCodes.ALT]), createSerializedKey(KeyCodes.UP, [KeyCodes.CTRL]), ], }, { name: Msg['MOVE_DOWN_UNCONSTRAINED'], preconditionFn: (workspace) => this.mover.isMoving(workspace), callback: (workspace) => this.mover.moveUnconstrained(workspace, Direction.Down), keyCodes: [ createSerializedKey(KeyCodes.DOWN, [KeyCodes.ALT]), createSerializedKey(KeyCodes.DOWN, [KeyCodes.CTRL]), ], }, ]; for (const shortcut of shortcuts) { ShortcutRegistry.registry.register(shortcut); this.shortcutNames.push(shortcut.name); } } private registerMenuItems() { const menuItems: ContextMenuRegistry.RegistryItem[] = [ { displayText: Msg['MOVE_BLOCK'].replace( '%1', getShortActionShortcut(Msg['START_MOVE']), ), preconditionFn: (scope, menuOpenEvent) => { const workspace = scope.block?.workspace as WorkspaceSvg | null; if (!workspace || menuOpenEvent instanceof PointerEvent) return 'hidden'; const startBlock = this.getCurrentBlock(workspace); return !!startBlock && this.mover.canMove(workspace, startBlock) ? 'enabled' : 'disabled'; }, callback: (scope) => { const workspace = scope.block?.workspace as WorkspaceSvg | null; if (!workspace) return false; const startBlock = this.getCurrentBlock(workspace); // Focus the start block in case one of its fields or a shadow block // was focused when the move was triggered. if (startBlock) { getFocusManager().focusNode(startBlock); } return ( !!startBlock && this.mover.startMove(workspace, startBlock, null) ); }, scopeType: ContextMenuRegistry.ScopeType.BLOCK, id: 'move', weight: 8.5, }, ]; for (const menuItem of menuItems) { ContextMenuRegistry.registry.register(menuItem); this.menuItemNames.push(menuItem.id); } } /** * Install the actions as both keyboard shortcuts and (where * applicable) context menu items. */ install() { this.registerShortcuts(); this.registerMenuItems(); } /** * Uninstall these actions. */ uninstall() { for (const shortcut of this.shortcutNames) { ShortcutRegistry.registry.unregister(shortcut); } for (const menuItem of this.menuItemNames) { ContextMenuRegistry.registry.unregister(menuItem); } } /** * Get the source block for the cursor location, or undefined if no * source block can be found. * If the cursor is on a shadow block, walks up the tree until it finds * a non-shadow block to drag. * * @param workspace The workspace to inspect for a cursor. * @returns The source block, or undefined if no appropriate block * could be found. */ getCurrentBlock(workspace: WorkspaceSvg): BlockSvg | undefined { let block = workspace?.getCursor()?.getSourceBlock(); if (!block) return undefined; while (block.isShadow()) { block = block.getParent(); if (!block) { throw new Error( 'Tried to drag a shadow block with no parent. ' + 'Shadow blocks should always have parents.', ); } } return block; } }