diff --git a/src/disabled_blocks.ts b/src/disabled_blocks.ts new file mode 100644 index 00000000..9fc3463b --- /dev/null +++ b/src/disabled_blocks.ts @@ -0,0 +1,84 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as Blockly from 'blockly/core'; + +const lastBlockDisabledReasons: Map> = new Map(); + +/** + * A change listener that enables disabled blocks when they + * are dragged, and re-disables them at the end of the drag. + * + * @param event Blockly event + */ +export function enableBlocksOnDrag(event: Blockly.Events.Abstract) { + // This listener only runs on Drag events that have a valid + // workspace and block id. + if (!isBlockDrag(event)) return; + if (!event.blockId) return; + const eventWorkspace = Blockly.common.getWorkspaceById( + event.workspaceId, + ) as Blockly.WorkspaceSvg; + const block = eventWorkspace.getBlockById(event.blockId); + if (!block) return; + + const oldUndo = Blockly.Events.getRecordUndo(); + Blockly.Events.setRecordUndo(false); + + if (event.isStart) { + // At start of drag, reset the lastBlockDisabledReasons + lastBlockDisabledReasons.clear(); + + // Enable all blocks including childeren + enableAllDraggedBlocks(block); + } else { + // Re-disable the block for its original reasons. If the block is no + // longer an orphan, the disableOrphans handler will enable the block. + redisableAllDraggedBlocks(block); + } + + Blockly.Events.setRecordUndo(oldUndo); +} + +/** + * Enables all blocks including children of the dragged blocks. + * Stores the reasons each block was disabled so they can be restored. + * + * @param block + */ +function enableAllDraggedBlocks(block: Blockly.BlockSvg) { + // getDescendants includes the block itself. + block.getDescendants(false).forEach((descendant) => { + const reasons = new Set(descendant.getDisabledReasons()); + lastBlockDisabledReasons.set(descendant.id, reasons); + reasons.forEach((reason) => descendant.setDisabledReason(false, reason)); + }); +} + +/** + * Re-disables all blocks using their original disabled reasons. + * + * @param block + */ +function redisableAllDraggedBlocks(block: Blockly.BlockSvg) { + block.getDescendants(false).forEach((descendant) => { + lastBlockDisabledReasons.get(descendant.id)?.forEach((reason) => { + descendant.setDisabledReason(true, reason); + }); + }); +} + +/** + * Type guard for drag events. + * + * @param event + * @returns true iff event.type is EventType.BLOCK_DRAG + */ +function isBlockDrag( + event: Blockly.Events.Abstract, +): event is Blockly.Events.BlockDrag { + return event.type === Blockly.Events.BLOCK_DRAG; +} diff --git a/src/index.ts b/src/index.ts index 8ece4f86..526b9d6c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,6 +7,7 @@ import * as Blockly from 'blockly/core'; import {NavigationController} from './navigation_controller'; import {getFlyoutElement, getToolboxElement} from './workspace_utilities'; +import {enableBlocksOnDrag} from './disabled_blocks'; /** Options object for KeyboardNavigation instances. */ export interface NavigationOptions { @@ -93,6 +94,9 @@ export class KeyboardNavigation { this.cursor = new Blockly.LineCursor(workspace, options.cursor); + // Add the event listener to enable disabled blocks on drag. + workspace.addChangeListener(enableBlocksOnDrag); + // Ensure that only the root SVG G (group) has a tab index. this.injectionDivTabIndex = workspace .getInjectionDiv() @@ -233,6 +237,9 @@ export class KeyboardNavigation { } } + // Remove the event listener that enables blocks on drag + this.workspace.removeChangeListener(enableBlocksOnDrag); + this.workspace.getSvgGroup().removeEventListener('blur', this.blurListener); this.workspace .getSvgGroup() diff --git a/test/index.ts b/test/index.ts index 7af7ca27..0e7687e9 100644 --- a/test/index.ts +++ b/test/index.ts @@ -23,6 +23,8 @@ import {javascriptGenerator} from 'blockly/javascript'; import {load} from './loadTestBlocks'; import {runCode, registerRunCodeShortcut} from './runCode'; +(window as any).Blockly = Blockly; + /** * Parse query params for inject and navigation options and update * the fields on the options form to match.