UNPKG

@blockly/keyboard-navigation

Version:
282 lines (268 loc) 9.4 kB
/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import { ShortcutRegistry, utils as BlocklyUtils, Field, keyboardNavigationController, } from 'blockly/core'; import type {Toolbox, WorkspaceSvg} from 'blockly/core'; import * as Blockly from 'blockly/core'; import * as Constants from '../constants'; import type {Navigation} from '../navigation'; const KeyCodes = BlocklyUtils.KeyCodes; /** * Class for registering shortcuts for navigating the workspace with arrow keys. */ export class ArrowNavigation { constructor(private navigation: Navigation) {} /** * Gives the cursor to the field to handle if the cursor is on a field. * * @param workspace The workspace to check. * @param shortcut The shortcut * to give to the field. * @returns True if the shortcut was handled by the field, false * otherwise. */ fieldShortcutHandler( workspace: WorkspaceSvg, shortcut: ShortcutRegistry.KeyboardShortcut, ): boolean { const cursor = workspace.getCursor(); if (!cursor || !cursor.getCurNode()) { return false; } const curNode = cursor.getCurNode(); if (curNode instanceof Field) { return curNode.onShortcut(shortcut); } return false; } /** * Adds all arrow key navigation shortcuts to the registry. */ install() { const navigateIn = ( workspace: WorkspaceSvg, e: Event, shortcut: ShortcutRegistry.KeyboardShortcut, ): boolean => { const toolbox = workspace.getToolbox() as Toolbox; const flyout = workspace.getFlyout(); let isHandled = false; switch (this.navigation.getState(workspace)) { case Constants.STATE.WORKSPACE: isHandled = this.fieldShortcutHandler(workspace, shortcut); if (!isHandled && workspace) { if ( !this.navigation.defaultWorkspaceCursorPositionIfNeeded(workspace) ) { workspace.getCursor()?.in(); } isHandled = true; } return isHandled; case Constants.STATE.TOOLBOX: // @ts-expect-error private method isHandled = toolbox && toolbox.selectChild(); if (!isHandled && flyout) { Blockly.getFocusManager().focusTree(flyout.getWorkspace()); this.navigation.defaultFlyoutCursorIfNeeded(workspace); } return true; default: return false; } }; const navigateOut = ( workspace: WorkspaceSvg, e: Event, shortcut: ShortcutRegistry.KeyboardShortcut, ): boolean => { const toolbox = workspace.getToolbox() as Toolbox; let isHandled = false; switch (this.navigation.getState(workspace)) { case Constants.STATE.WORKSPACE: isHandled = this.fieldShortcutHandler(workspace, shortcut); if (!isHandled && workspace) { if ( !this.navigation.defaultWorkspaceCursorPositionIfNeeded(workspace) ) { workspace.getCursor()?.out(); } isHandled = true; } return isHandled; case Constants.STATE.FLYOUT: if (toolbox) { Blockly.getFocusManager().focusTree(toolbox); } return true; case Constants.STATE.TOOLBOX: // @ts-expect-error private method return toolbox && toolbox.selectParent(); default: return false; } }; const shortcuts: { [name: string]: ShortcutRegistry.KeyboardShortcut; } = { /** Go to the next location to the right. */ right: { name: Constants.SHORTCUT_NAMES.RIGHT, preconditionFn: (workspace) => this.navigation.canCurrentlyNavigate(workspace), callback: (workspace, e, shortcut) => { keyboardNavigationController.setIsActive(true); return workspace.RTL ? navigateOut(workspace, e, shortcut) : navigateIn(workspace, e, shortcut); }, keyCodes: [KeyCodes.RIGHT], }, /** Go to the next location to the left. */ left: { name: Constants.SHORTCUT_NAMES.LEFT, preconditionFn: (workspace) => this.navigation.canCurrentlyNavigate(workspace), callback: (workspace, e, shortcut) => { keyboardNavigationController.setIsActive(true); return workspace.RTL ? navigateIn(workspace, e, shortcut) : navigateOut(workspace, e, shortcut); }, keyCodes: [KeyCodes.LEFT], }, /** Go down to the next location. */ down: { name: Constants.SHORTCUT_NAMES.DOWN, preconditionFn: (workspace) => this.navigation.canCurrentlyNavigate(workspace), callback: (workspace, e, shortcut) => { keyboardNavigationController.setIsActive(true); const toolbox = workspace.getToolbox() as Toolbox; const flyout = workspace.getFlyout(); let isHandled = false; switch (this.navigation.getState(workspace)) { case Constants.STATE.WORKSPACE: isHandled = this.fieldShortcutHandler(workspace, shortcut); if (!isHandled && workspace) { if ( !this.navigation.defaultWorkspaceCursorPositionIfNeeded( workspace, ) ) { workspace.getCursor()?.next(); } isHandled = true; } return isHandled; case Constants.STATE.FLYOUT: isHandled = this.fieldShortcutHandler(workspace, shortcut); if (!isHandled && flyout) { if (!this.navigation.defaultFlyoutCursorIfNeeded(workspace)) { flyout.getWorkspace()?.getCursor()?.next(); } isHandled = true; } return isHandled; case Constants.STATE.TOOLBOX: if (toolbox) { if (!toolbox.getSelectedItem()) { const firstItem = toolbox .getToolboxItems() .find((item) => item.isSelectable()) ?? null; toolbox.setSelectedItem(firstItem); isHandled = true; } else { // @ts-expect-error private method isHandled = toolbox.selectNext(); } const selectedItem = toolbox.getSelectedItem(); if (selectedItem) { Blockly.getFocusManager().focusNode(selectedItem); } } return isHandled; default: return false; } }, keyCodes: [KeyCodes.DOWN], }, /** Go up to the previous location. */ up: { name: Constants.SHORTCUT_NAMES.UP, preconditionFn: (workspace) => this.navigation.canCurrentlyNavigate(workspace), callback: (workspace, e, shortcut) => { keyboardNavigationController.setIsActive(true); const flyout = workspace.getFlyout(); const toolbox = workspace.getToolbox() as Toolbox; let isHandled = false; switch (this.navigation.getState(workspace)) { case Constants.STATE.WORKSPACE: isHandled = this.fieldShortcutHandler(workspace, shortcut); if (!isHandled) { if ( !this.navigation.defaultWorkspaceCursorPositionIfNeeded( workspace, 'last', ) ) { workspace.getCursor()?.prev(); } isHandled = true; } return isHandled; case Constants.STATE.FLYOUT: isHandled = this.fieldShortcutHandler(workspace, shortcut); if (!isHandled && flyout) { if ( !this.navigation.defaultFlyoutCursorIfNeeded( workspace, 'last', ) ) { flyout.getWorkspace()?.getCursor()?.prev(); } isHandled = true; } return isHandled; case Constants.STATE.TOOLBOX: if (toolbox) { // @ts-expect-error private method isHandled = toolbox.selectPrevious(); const selectedItem = toolbox.getSelectedItem(); if (selectedItem) { Blockly.getFocusManager().focusNode(selectedItem); } } return isHandled; default: return false; } }, keyCodes: [KeyCodes.UP], }, }; for (const shortcut of Object.values(shortcuts)) { ShortcutRegistry.registry.register(shortcut); } } /** * Removes all the arrow navigation shortcuts. */ uninstall() { ShortcutRegistry.registry.unregister(Constants.SHORTCUT_NAMES.LEFT); ShortcutRegistry.registry.unregister(Constants.SHORTCUT_NAMES.RIGHT); ShortcutRegistry.registry.unregister(Constants.SHORTCUT_NAMES.DOWN); ShortcutRegistry.registry.unregister(Constants.SHORTCUT_NAMES.UP); } }