/** * @license * Copyright 2020 Google LLC * SPDX-License-Identifier: Apache-2.0 */ /** * @fileoverview Registers default context menu items. * @author maribethb@google.com (Maribeth Bottorff) */ 'use strict'; /** * @name Blockly.ContextMenuItems * @namespace */ goog.provide('Blockly.ContextMenuItems'); goog.require('Blockly.constants'); goog.require('Blockly.Events'); goog.requireType('Blockly.BlockSvg'); /** Option to undo previous action. */ Blockly.ContextMenuItems.registerUndo = function() { /** @type {!Blockly.ContextMenuRegistry.RegistryItem} */ var undoOption = { displayText: function() { return Blockly.Msg['UNDO']; }, preconditionFn: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ scope) { if (scope.workspace.getUndoStack().length > 0) { return 'enabled'; } return 'disabled'; }, callback: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ scope) { scope.workspace.undo(false); }, scopeType: Blockly.ContextMenuRegistry.ScopeType.WORKSPACE, id: 'undoWorkspace', weight: 1, }; Blockly.ContextMenuRegistry.registry.register(undoOption); }; /** Option to redo previous action. */ Blockly.ContextMenuItems.registerRedo = function() { /** @type {!Blockly.ContextMenuRegistry.RegistryItem} */ var redoOption = { displayText: function() { return Blockly.Msg['REDO']; }, preconditionFn: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ scope) { if (scope.workspace.getRedoStack().length > 0) { return 'enabled'; } return 'disabled'; }, callback: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ scope) { scope.workspace.undo(true); }, scopeType: Blockly.ContextMenuRegistry.ScopeType.WORKSPACE, id: 'redoWorkspace', weight: 2, }; Blockly.ContextMenuRegistry.registry.register(redoOption); }; /** Option to clean up blocks. */ Blockly.ContextMenuItems.registerCleanup = function() { /** @type {!Blockly.ContextMenuRegistry.RegistryItem} */ var cleanOption = { displayText: function() { return Blockly.Msg['CLEAN_UP']; }, preconditionFn: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ scope) { if (scope.workspace.isMovable()) { if (scope.workspace.getTopBlocks(false).length > 1) { return 'enabled'; } return 'disabled'; } return 'hidden'; }, callback: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ scope) { scope.workspace.cleanUp(); }, scopeType: Blockly.ContextMenuRegistry.ScopeType.WORKSPACE, id: 'cleanWorkspace', weight: 3, }; Blockly.ContextMenuRegistry.registry.register(cleanOption); }; /** * Creates a callback to collapse or expand top blocks. * @param {boolean} shouldCollapse Whether a block should collapse. * @param {!Array} topBlocks Top blocks in the workspace. * @private */ Blockly.ContextMenuItems.toggleOption_ = function(shouldCollapse, topBlocks) { var DELAY = 10; var ms = 0; for (var i = 0; i < topBlocks.length; i++) { var block = topBlocks[i]; while (block) { setTimeout(block.setCollapsed.bind(block, shouldCollapse), ms); block = block.getNextBlock(); ms += DELAY; } } }; /** Option to collapse all blocks. */ Blockly.ContextMenuItems.registerCollapse = function() { /** @type {!Blockly.ContextMenuRegistry.RegistryItem} */ var collapseOption = { displayText: function() { return Blockly.Msg['COLLAPSE_ALL']; }, preconditionFn: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ scope) { if (scope.workspace.options.collapse) { var topBlocks = scope.workspace.getTopBlocks(false); for (var i = 0; i < topBlocks.length; i++) { var block = topBlocks[i]; while (block) { if (!block.isCollapsed()) { return 'enabled'; } block = block.getNextBlock(); } } return 'disabled'; } return 'hidden'; }, callback: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ scope) { Blockly.ContextMenuItems.toggleOption_(true, scope.workspace.getTopBlocks(true)); }, scopeType: Blockly.ContextMenuRegistry.ScopeType.WORKSPACE, id: 'collapseWorkspace', weight: 4, }; Blockly.ContextMenuRegistry.registry.register(collapseOption); }; /** Option to expand all blocks. */ Blockly.ContextMenuItems.registerExpand = function() { /** @type {!Blockly.ContextMenuRegistry.RegistryItem} */ var expandOption = { displayText: function() { return Blockly.Msg['EXPAND_ALL']; }, preconditionFn: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ scope) { if (scope.workspace.options.collapse) { var topBlocks = scope.workspace.getTopBlocks(false); for (var i = 0; i < topBlocks.length; i++) { var block = topBlocks[i]; while (block) { if (block.isCollapsed()) { return 'enabled'; } block = block.getNextBlock(); } } return 'disabled'; } return 'hidden'; }, callback: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ scope) { Blockly.ContextMenuItems.toggleOption_(false, scope.workspace.getTopBlocks(true)); }, scopeType: Blockly.ContextMenuRegistry.ScopeType.WORKSPACE, id: 'expandWorkspace', weight: 5, }; Blockly.ContextMenuRegistry.registry.register(expandOption); }; /** * Adds a block and its children to a list of deletable blocks. * @param {!Blockly.BlockSvg} block to delete. * @param {!Array.} deleteList list of blocks that can be deleted. This will be * modifed in place with the given block and its descendants. * @private */ Blockly.ContextMenuItems.addDeletableBlocks_ = function(block, deleteList) { if (block.isDeletable()) { Array.prototype.push.apply(deleteList, block.getDescendants(false)); } else { var children = /** @type !Array. */ (block.getChildren(false)); for (var i = 0; i < children.length; i++) { Blockly.ContextMenuItems.addDeletableBlocks_(children[i], deleteList); } } }; /** * Constructs a list of blocks that can be deleted in the given workspace. * @param {!Blockly.WorkspaceSvg} workspace to delete all blocks from. * @return {!Array.} list of blocks to delete. * @private */ Blockly.ContextMenuItems.getDeletableBlocks_ = function(workspace) { var deleteList = []; var topBlocks = workspace.getTopBlocks(true); for (var i = 0; i < topBlocks.length; i++) { Blockly.ContextMenuItems.addDeletableBlocks_(topBlocks[i], deleteList); } return deleteList; }; /** Deletes the given blocks. Used to delete all blocks in the workspace. * @param {!Array.} deleteList list of blocks to delete. * @param {string} eventGroup event group id with which all delete events should be associated. * @private */ Blockly.ContextMenuItems.deleteNext_ = function(deleteList, eventGroup) { var DELAY = 10; Blockly.Events.setGroup(eventGroup); var block = deleteList.shift(); if (block) { if (block.workspace) { block.dispose(false, true); setTimeout(Blockly.ContextMenuItems.deleteNext_, DELAY, deleteList, eventGroup); } else { Blockly.ContextMenuItems.deleteNext_(deleteList, eventGroup); } } Blockly.Events.setGroup(false); }; /** Option to delete all blocks. */ Blockly.ContextMenuItems.registerDeleteAll = function() { /** @type {!Blockly.ContextMenuRegistry.RegistryItem} */ var deleteOption = { displayText: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ scope) { if (!scope.workspace) { return; } var deletableBlocksLength = Blockly.ContextMenuItems.getDeletableBlocks_(scope.workspace).length; if (deletableBlocksLength == 1) { return Blockly.Msg['DELETE_BLOCK']; } else { return Blockly.Msg['DELETE_X_BLOCKS'].replace('%1', String(deletableBlocksLength)); } }, preconditionFn: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ scope) { if (!scope.workspace) { return; } var deletableBlocksLength = Blockly.ContextMenuItems.getDeletableBlocks_(scope.workspace).length; return deletableBlocksLength > 0 ? 'enabled' : 'disabled'; }, callback: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ scope) { if (!scope.workspace) { return; } scope.workspace.cancelCurrentGesture(); var deletableBlocks = Blockly.ContextMenuItems.getDeletableBlocks_(scope.workspace); var eventGroup = Blockly.utils.genUid(); if (deletableBlocks.length < 2) { Blockly.ContextMenuItems.deleteNext_(deletableBlocks, eventGroup); } else { Blockly.confirm( Blockly.Msg['DELETE_ALL_BLOCKS'].replace('%1', deletableBlocks.length), function(ok) { if (ok) { Blockly.ContextMenuItems.deleteNext_(deletableBlocks, eventGroup); } }); } }, scopeType: Blockly.ContextMenuRegistry.ScopeType.WORKSPACE, id: 'workspaceDelete', weight: 6, }; Blockly.ContextMenuRegistry.registry.register(deleteOption); }; /** * Registers all workspace-scoped context menu items. * @private */ Blockly.ContextMenuItems.registerWorkspaceOptions_ = function() { Blockly.ContextMenuItems.registerUndo(); Blockly.ContextMenuItems.registerRedo(); Blockly.ContextMenuItems.registerCleanup(); Blockly.ContextMenuItems.registerCollapse(); Blockly.ContextMenuItems.registerExpand(); Blockly.ContextMenuItems.registerDeleteAll(); }; /** Option to duplicate a block. */ Blockly.ContextMenuItems.registerDuplicate = function() { /** @type {!Blockly.ContextMenuRegistry.RegistryItem} */ var duplicateOption = { displayText: function() { return Blockly.Msg['DUPLICATE_BLOCK']; }, preconditionFn: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ scope) { var block = scope.block; if (!block.isInFlyout && block.isDeletable() && block.isMovable()) { if (block.isDuplicatable()) { return 'enabled'; } return 'disabled'; } return 'hidden'; }, callback: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ scope) { if (scope.block) { Blockly.duplicate(scope.block); } }, scopeType: Blockly.ContextMenuRegistry.ScopeType.BLOCK, id: 'blockDuplicate', weight: 1, }; Blockly.ContextMenuRegistry.registry.register(duplicateOption); }; /** Option to add or remove block-level comment. */ Blockly.ContextMenuItems.registerComment = function() { /** @type {!Blockly.ContextMenuRegistry.RegistryItem} */ var commentOption = { displayText: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ scope) { if (scope.block.getCommentIcon()) { // If there's already a comment, option is to remove. return Blockly.Msg['REMOVE_COMMENT']; } // If there's no comment yet, option is to add. return Blockly.Msg['ADD_COMMENT']; }, preconditionFn: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ scope) { var block = scope.block; // IE doesn't support necessary features for comment editing. if (!Blockly.utils.userAgent.IE && !block.isInFlyout && block.workspace.options.comments && !block.isCollapsed() && block.isEditable()) { return 'enabled'; } return 'hidden'; }, callback: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ scope) { var block = scope.block; if (block.getCommentIcon()) { block.setCommentText(null); } else { block.setCommentText(''); } }, scopeType: Blockly.ContextMenuRegistry.ScopeType.BLOCK, id: 'blockComment', weight: 2, }; Blockly.ContextMenuRegistry.registry.register(commentOption); }; /** Option to inline variables. */ Blockly.ContextMenuItems.registerInline = function() { /** @type {!Blockly.ContextMenuRegistry.RegistryItem} */ var inlineOption = { displayText: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ scope) { return (scope.block.getInputsInline()) ? Blockly.Msg['EXTERNAL_INPUTS'] : Blockly.Msg['INLINE_INPUTS']; }, preconditionFn: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ scope) { var block = scope.block; if (!block.isInFlyout && block.isMovable() && !block.isCollapsed()) { for (var i = 1; i < block.inputList.length; i++) { // Only display this option if there are two value or dummy inputs next to each other. if (block.inputList[i - 1].type != Blockly.NEXT_STATEMENT && block.inputList[i].type != Blockly.NEXT_STATEMENT) { return 'enabled'; } } } return 'hidden'; }, callback: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ scope) { scope.block.setInputsInline(!scope.block.getInputsInline()); }, scopeType: Blockly.ContextMenuRegistry.ScopeType.BLOCK, id: 'blockInline', weight: 3, }; Blockly.ContextMenuRegistry.registry.register(inlineOption); }; /** Option to collapse or expand a block. */ Blockly.ContextMenuItems.registerCollapseExpandBlock = function() { /** @type {!Blockly.ContextMenuRegistry.RegistryItem} */ var collapseExpandOption = { displayText: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ scope) { if (scope.block.isCollapsed()) { return Blockly.Msg['EXPAND_BLOCK']; } return Blockly.Msg['COLLAPSE_BLOCK']; }, preconditionFn: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ scope) { var block = scope.block; if (!block.isInFlyout && block.isMovable()) { return 'enabled'; } return 'hidden'; }, callback: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ scope) { scope.block.setCollapsed(!scope.block.isCollapsed()); }, scopeType: Blockly.ContextMenuRegistry.ScopeType.BLOCK, id: 'blockCollapseExpand', weight: 4, }; Blockly.ContextMenuRegistry.registry.register(collapseExpandOption); }; /** Option to disable or enable a block. */ Blockly.ContextMenuItems.registerDisable = function() { /** @type {!Blockly.ContextMenuRegistry.RegistryItem} */ var disableOption = { displayText: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ scope) { return (scope.block.isEnabled()) ? Blockly.Msg['DISABLE_BLOCK'] : Blockly.Msg['ENABLE_BLOCK']; }, preconditionFn: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ scope) { var block = scope.block; if (!block.isInFlyout && block.workspace.options.disable && block.isEditable()) { if (block.getInheritedDisabled()) { return 'disabled'; } return 'enabled'; } return 'hidden'; }, callback: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ scope) { var block = scope.block; var group = Blockly.Events.getGroup(); if (!group) { Blockly.Events.setGroup(true); } block.setEnabled(!block.isEnabled()); if (!group) { Blockly.Events.setGroup(false); } }, scopeType: Blockly.ContextMenuRegistry.ScopeType.BLOCK, id: 'blockDisable', weight: 5, }; Blockly.ContextMenuRegistry.registry.register(disableOption); }; /** Option to delete a block. */ Blockly.ContextMenuItems.registerDelete = function() { /** @type {!Blockly.ContextMenuRegistry.RegistryItem} */ var deleteOption = { displayText: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ scope) { var block = scope.block; // Count the number of blocks that are nested in this block. var descendantCount = block.getDescendants(false).length; var nextBlock = block.getNextBlock(); if (nextBlock) { // Blocks in the current stack would survive this block's deletion. descendantCount -= nextBlock.getDescendants(false).length; } return (descendantCount == 1) ? Blockly.Msg['DELETE_BLOCK'] : Blockly.Msg['DELETE_X_BLOCKS'].replace('%1', String(descendantCount)); }, preconditionFn: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ scope) { if (!scope.block.isInFlyout && scope.block.isDeletable()) { return 'enabled'; } return 'hidden'; }, callback: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ 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); }; /** Option to open help for a block. */ Blockly.ContextMenuItems.registerHelp = function() { /** @type {!Blockly.ContextMenuRegistry.RegistryItem} */ var helpOption = { displayText: function() { return Blockly.Msg['HELP']; }, preconditionFn: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ scope) { var block = scope.block; var url = (typeof block.helpUrl == 'function') ? block.helpUrl() : block.helpUrl; if (url) { return 'enabled'; } return 'hidden'; }, callback: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ scope) { scope.block.showHelp(); }, scopeType: Blockly.ContextMenuRegistry.ScopeType.BLOCK, id: 'blockHelp', weight: 7, }; Blockly.ContextMenuRegistry.registry.register(helpOption); }; /** * Registers all block-scoped context menu items. * @private */ Blockly.ContextMenuItems.registerBlockOptions_ = function() { Blockly.ContextMenuItems.registerDuplicate(); Blockly.ContextMenuItems.registerComment(); Blockly.ContextMenuItems.registerInline(); Blockly.ContextMenuItems.registerCollapseExpandBlock(); Blockly.ContextMenuItems.registerDisable(); Blockly.ContextMenuItems.registerDelete(); Blockly.ContextMenuItems.registerHelp(); }; /** * Registers all default context menu items. This should be called once per instance of * ContextMenuRegistry. * @package */ Blockly.ContextMenuItems.registerDefaultOptions = function() { Blockly.ContextMenuItems.registerWorkspaceOptions_(); Blockly.ContextMenuItems.registerBlockOptions_(); };