From a65d24a38daf8a740d5230ee9a7fbac5ae70e2af Mon Sep 17 00:00:00 2001 From: Beka Westberg Date: Wed, 21 Aug 2024 16:22:47 +0000 Subject: [PATCH] fix: delete context menu to display the correct number of blocks (#127) --- src/context_menu_items.js | 146 ++++++++++++++++++++++++++++++++++++++ src/index.js | 6 ++ 2 files changed, 152 insertions(+) create mode 100644 src/context_menu_items.js diff --git a/src/context_menu_items.js b/src/context_menu_items.js new file mode 100644 index 0000000000..aca1ae0796 --- /dev/null +++ b/src/context_menu_items.js @@ -0,0 +1,146 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as Blockly from "blockly/core"; + +/** + * Registers a block delete option that ignores shadows in the block count. + */ +export function registerDeleteBlock() { + const deleteOption = { + displayText(scope) { + const descendantCount = getDeletableBlocksInStack(scope.block).length; + return descendantCount === 1 + ? Blockly.Msg["DELETE_BLOCK"] + : Blockly.Msg["DELETE_X_BLOCKS"].replace("%1", `${descendantCount}`); + }, + preconditionFn(scope) { + if (!scope.block.isInFlyout && scope.block.isDeletable()) { + return "enabled"; + } + return "hidden"; + }, + callback(scope) { + Blockly.Events.setGroup(true); + scope.block.dispose(true, true); + Blockly.Events.setGroup(false); + }, + scopeType: Blockly.ContextMenuRegistry.ScopeType.BLOCK, + id: "blockDelete", + weight: 6, + }; + Blockly.ContextMenuRegistry.registry.register(deleteOption); +} + +function getDeletableBlocksInStack(block) { + let descendants = block.getDescendants(false).filter(isDeletable); + if (block.getNextBlock()) { + // Next blocks are not deleted. + const nextDescendants = block + .getNextBlock() + .getDescendants(false) + .filter(isDeletable); + descendants = descendants.filter((b) => !nextDescendants.includes(b)); + } + return descendants; +} + +function isDeletable(block) { + return block.isDeletable() && !block.isShadow(); +} + +/** + * Option to delete all blocks. + */ +export function registerDeleteAll() { + const deleteOption = { + displayText(scope) { + if (!scope.workspace) { + return ""; + } + const deletableBlocksLength = getDeletableBlocksInWorkspace(scope.workspace).length; + if (deletableBlocksLength === 1) { + return Blockly.Msg["DELETE_BLOCK"]; + } + return Blockly.Msg["DELETE_X_BLOCKS"].replace( + "%1", + `${deletableBlocksLength}` + ); + }, + preconditionFn(scope) { + if (!scope.workspace) { + return "disabled"; + } + const deletableBlocksLength = getDeletableBlocksInWorkspace(scope.workspace).length; + return deletableBlocksLength > 0 ? "enabled" : "disabled"; + }, + callback(scope) { + if (!scope.workspace) { + return; + } + scope.workspace.cancelCurrentGesture(); + const deletableBlocks = getDeletableBlocksInWorkspace(scope.workspace); + if (deletableBlocks.length < 2) { + deleteNext(deletableBlocks); + } else { + Blockly.dialog.confirm( + Blockly.Msg["DELETE_ALL_BLOCKS"].replace( + "%1", + String(deletableBlocks.length) + ), + function (ok) { + if (ok) { + deleteNext(deletableBlocks); + } + } + ); + } + }, + scopeType: Blockly.ContextMenuRegistry.ScopeType.WORKSPACE, + id: "workspaceDelete", + weight: 6, + }; + Blockly.ContextMenuRegistry.registry.register(deleteOption); +} + +/* + * Constructs a list of blocks that can be deleted in the given workspace. + * + * @param workspace to delete all blocks from. + * @returns list of blocks to delete. + */ +function getDeletableBlocksInWorkspace(workspace) { + return workspace + .getTopBlocks(true) + .flatMap((b) => b.getDescendants(false).filter(isDeletable)); +} + +/** + * Deletes the given blocks. Used to delete all blocks in the workspace. + * + * @param deleteList List of blocks to delete. + * @param eventGroup Event group ID with which all delete events should be + * associated. If not specified, create a new group. + */ +function deleteNext(deleteList, eventGroup) { + const DELAY = 10; + if (eventGroup) { + Blockly.Events.setGroup(eventGroup); + } else { + Blockly.Events.setGroup(true); + eventGroup = Blockly.Events.getGroup(); + } + const block = deleteList.shift(); + if (block) { + if (!block.isDeadOrDying()) { + block.dispose(false, true); + setTimeout(deleteNext, DELAY, deleteList, eventGroup); + } else { + deleteNext(deleteList, eventGroup); + } + } + Blockly.Events.setGroup(false); +} diff --git a/src/index.js b/src/index.js index 4621666f12..7a6182431e 100644 --- a/src/index.js +++ b/src/index.js @@ -27,6 +27,7 @@ import * as ScratchVariables from "./variables.js"; import "../core/css.js"; import "../core/field_vertical_separator.js"; import "./renderer/renderer.js"; +import * as contextMenuItems from "./context_menu_items.js"; import { ContinuousToolbox, ContinuousFlyout, @@ -67,6 +68,7 @@ export { glowStack }; export { scratchBlocksUtils }; export { CheckableContinuousFlyout }; export { ScratchVariables }; +export { contextMenuItems }; export function inject(container, options) { Object.assign(options, { @@ -105,3 +107,7 @@ Blockly.FlyoutButton.TEXT_MARGIN_Y = 10; Blockly.ContextMenuRegistry.registry.unregister("blockDisable"); Blockly.ContextMenuRegistry.registry.unregister("blockInline"); Blockly.ContextMenuItems.registerCommentOptions(); +Blockly.ContextMenuRegistry.registry.unregister("blockDelete"); +contextMenuItems.registerDeleteBlock(); +Blockly.ContextMenuRegistry.registry.unregister("workspaceDelete"); +contextMenuItems.registerDeleteAll();