From 70cacf18a0e16bca09a37b8fabfd0035bad053f3 Mon Sep 17 00:00:00 2001 From: Ronald Bourret Date: Wed, 22 Oct 2025 20:47:50 +0000 Subject: [PATCH 1/9] docs: Update context menu codelab for v12 --- .../context_menu_option.md | 229 +++++++++--------- .../complete-code/index.js | 112 +++++---- .../starter-code/index.js | 1 + 3 files changed, 171 insertions(+), 171 deletions(-) diff --git a/codelabs/context_menu_option/context_menu_option.md b/codelabs/context_menu_option/context_menu_option.md index d3647b13c2..4622232db7 100644 --- a/codelabs/context_menu_option/context_menu_option.md +++ b/codelabs/context_menu_option/context_menu_option.md @@ -15,6 +15,7 @@ In this codelab you will learn how to: - Add a context menu option to the workspace. - Add a context menu option to all blocks. - Use precondition functions to hide or disable context menu options. +- Take an action when a menu option is clicked. - Customize ordering and display text for context menu options. ### What you'll build @@ -55,40 +56,38 @@ To run the code, simple open `starter-code/index.html` in a browser. You should ![A web page with the text "Context Menu Codelab" and a simple Blockly workspace.](starter_workspace.png) -## Add a context menu option to the workspace +## Add a context menu item -In this section you will create a very basic `Blockly.ContextMenuRegistry.RegistryItem`, then register it to display when you right-click on the workspace. +In this section you will create a very basic `Blockly.ContextMenuRegistry.RegistryItem`, then register it to display when you right-click on the workspace, a block, or a comment. ### The RegistryItem -Blockly stores context menu options as items in a registry. When the user right-clicks, Blockly queries the registry for a list of context menu options that should be displayed. +A context menu consists of one or more menu options that a user can click. Blockly stores information about menu option as items in a registry. You can think of the _registry items_ as templates for constructing _menu options_. When the user right-clicks, Blockly retrieves all of the registry items that apply to the current context and uses them to construct a list of menu options. -Each menu option in the registry has several properties: +Each item in the registry has several properties: -- `callback`: A function called when the menu option is clicked. -- `scopeType`: An enum indicating when this option should be shown. - `displayText`: The text to show in the menu. Either a string, or HTML, or a function that returns either of the former. - `preconditionFn`: Function that returns one of `'enabled'`, `'disabled'`, or `'hidden'` to determine whether and how the menu option should be rendered. +- `callback`: A function called when the menu option is clicked. +- `id`: A unique string id for the item. - `weight`: A number that determines the sort order of the option. Options with higher weights appear later in the context menu. -- `id`: A unique string id for the option. We will discuss these in detail in later sections of the codelab. ### Make a RegistryItem -Add a function to `index.js` named `registerFirstContextMenuOptions`. Create a new registry item in your function: +Add a function to `index.js` named `registerHelloWorldItem`. Create a new registry item in your function: ```js -function registerFirstContextMenuOptions() { - const workspaceItem = { +function registerHelloWorldItem() { + const helloWorldItem = { displayText: 'Hello World', preconditionFn: function(scope) { return 'enabled'; }, callback: function(scope) { }, - scopeType: Blockly.ContextMenuRegistry.ScopeType.WORKSPACE, id: 'hello_world', weight: 100, }; @@ -99,7 +98,9 @@ Call your function from `start`: ```js function start() { - registerFirstContextMenuOptions(); + registerHelloWorldItem(); + + Blockly.ContextMenuItems.registerCommentOptions(); // Create main workspace. workspace = Blockly.inject('blocklyDiv', { @@ -113,11 +114,11 @@ function start() { Next, register your item with Blockly: ```js -function registerFirstContextMenuOptions() { - const workspaceItem = { +function registerHelloWorldItem() { + const helloWorldItem = { // ... }; - Blockly.ContextMenuRegistry.registry.register(workspaceItem); + Blockly.ContextMenuRegistry.registry.register(helloWorldItem); } ``` @@ -125,139 +126,106 @@ Note: you will never need to make a new `ContextMenuRegistry`. Always use the si ### Test it -Reload your web page and right-click on the workspace. You should see a new item labeled "Hello World" at the bottom of the context menu. - - -![A context menu. The last item says "Hello World".](hello_world.png) - -## Scope type - -Every context menu option is registered with a **scope type**, which is either `Blockly.ContextMenuRegistry.ScopeType.BLOCK`, or `Blockly.ContextMenuRegistry.ScopeType.COMMENT`, or `Blockly.ContextMenuRegistry.ScopeType.WORKSPACE`. The scope type determines: - -- Where the option should be show. -- What information is passed to the precondition and callback functions. +Reload your web page and right-click on the workspace. You should see a new option labeled "Hello World" at the bottom of the context menu. -### Add to block scope -You registered your context menu option on the workspace scope but not the block scope. As a result, you will see it when you right-click on the workspace but not when you right-click on a block. +![A context menu. The last option says "Hello World".](hello_world.png) -If you want your option to be shown for both workspaces and blocks, you must register it once for each scope type. Add code to `registerFirstContextMenuOptions` to copy and re-register the workspace item: +Next, drag a block onto the workspace and right-click on the block. You'll see "Hello World" at the bottom of the block's context menu. Finally, right-click on the workspace and create a comment, then right-click on the comment's header. "Hello World" should be at the bottom of the context menu. -```js - let blockItem = {...workspaceItem} - blockItem.scopeType = Blockly.ContextMenuRegistry.ScopeType.BLOCK; - blockItem.id = 'hello_world_block'; - Blockly.ContextMenuRegistry.registry.register(blockItem); -``` - -Notice that this code uses the JavaScript spread operator to copy the original item object, then replaces the scope type and id. Simply updating `workspaceItem` and re-registering it would modify the original registry item in place, leading to unintended behaviour. - -### Test it - -Drag a block into the workspace and right-click it. You should see a "Hello world" option on the block context menu. +## Precondition: Node type -![An if block with a context menu with five items. The last item says "Hello World".](hello_world_block.png) +Each registry item has a `preconditionFn`. It is called by Blockly to decide whether and how to display an option on a context menu. You'll use it to display the "Hello, World" option on workspace and block context menus, but not on comment context menus. -## Precondition +### The scope argument -Each registry item has a `preconditionFn`. This function takes in a scope and returns a string -indicating whether and how to display the context menu option. We will discuss the scope in the next section. +The `scope` argument is an object that is passed to `preconditionFn`. You'll use the `scope.focusedNode` property to determine which object the context menu was invoked on. Why a focused node? Because all Blockly components that support context menus implement the `IFocusableNode` interface, so it's a handy way to pass objects (like workspaces, blocks, and comments) that otherwise have nothing in common. ### Return value -The return value should be one of `'enabled'`, `'disabled'`, or `'hidden'`. +The return value of `preconditionFn` is `'enabled'`, `'disabled'`, or `'hidden'`. An **enabled** option is shown with black text and is clickable. A **disabled** option is shown with grey text and is not clickable. A **hidden** option is not included in the context menu at all. -An **enabled** option is shown with black text and is clickable. A **disabled** option is shown with grey text and is not clickable. A **hidden** option is not included in the context menu at all. +### Write the function -For instance, let's disable `workspaceItem` for the second half of every minute: +You can now test `scope.focusedNode` to display the "Hello World" option in workspace and block context menus, but not on any others. Change `preconditionFn` to: ```js -preconditionFn: function(scope) { - const now = new Date(Date.now()); - if (now.getSeconds() < 30) { - return 'enabled'; - } - return 'disabled'; -} + const helloWorldItem = { + ... + preconditionFn: function (scope) { + if (scope.focusedNode instanceof Blockly.WorkspaceSvg || + scope.focusedNode instanceof Blockly.BlockSvg) { + return 'enabled'; + } + return 'hidden'; + }, + ... + }; ``` ### Test it -Reload your workspace, grab a stopwatch, and right-click to confirm the timing. The item will always be in the menu, but will sometimes be greyed out. - -![A context menu. The last item says "Hello World" but the text is grey, indicating that it cannot be selected.](hello_world_grey.png) - -## Scope - -Disabling your context menu options half of the time is not useful, but you may want to show or hide an option based on what the user is doing in the workspace. +Right-click the workspace, a block, and a comment. You should see a "Hello World" option on the workspace and block context menus, but not on the comment context menu. -To do that you'll need to use the `scope` argument to `preconditionFn`. `scope` is a `Blockly.ContextMenuRegistry.Scope` object. It contains three properties, `workspace`, `block`, and `comment`, but only one is set at any time: +![An if block with a context menu with five options. The last option says "Hello World".](hello_world_block.png) -- If your item is registered under the `WORKSPACE` scope type you can access the `workspace` property, which is an instance of `Blockly.WorkspaceSvg`. -- If registered under the `BLOCK` scope type you can access the `block` property, which is an instance of `Blockly.BlockSvg`. -- If registered under the `COMMENT` scope type you can access the `comment` property, which is an instance of `Blockly.RenderedWorkspaceComment`. +## Precondition: External state -### Workspace scope - -For example, let's show a **Help** option in the context menu if the user doesn't have any blocks on the workspace. Add this code in `index.js`: +Use of the `preconditionFn` is not limited to checking the type of the Blockly component that the context menu was invoked on. You can use it to check for conditions entirely outside of Blockly. For instance, let's disable `helloWorldItem` for the second half of every minute: ```js -function registerHelpOption() { - const helpItem = { - displayText: 'Help! There are no blocks', - preconditionFn: function(scope) { - if (!scope.workspace.getTopBlocks().length) { - return 'enabled'; + preconditionFn: function (scope) { + if (scope.focusedNode instanceof Blockly.WorkspaceSvg || + scope.focusedNode instanceof Blockly.BlockSvg) { + const now = new Date(Date.now()); + if (now.getSeconds() < 30) { + return 'enabled'; + } + return 'disabled'; } return 'hidden'; }, - callback: function(scope) { - }, - scopeType: Blockly.ContextMenuRegistry.ScopeType.WORKSPACE, - id: 'help_no_blocks', - weight: 100, - }; - Blockly.ContextMenuRegistry.registry.register(helpItem); -} ``` -The precondition function accesses `scope.workspace` and uses it to check whether there are any blocks on the workspace. +### Test it + +Reload your workspace, check your watch, and right-click on the workspace to confirm the timing. The option will always be in the menu, but will sometimes be greyed out. + +![A context menu. The last option says "Hello World" but the text is grey, indicating that it cannot be selected.](hello_world_grey.png) -### Block scope +## Precondition: Blockly state -To demonstrate block scope, add an option that is only visible when the block has an output connection: +Disabling your context menu options half of the time is not useful, but you may want to show or hide an option based on what the user is doing. For example, let's show a **Help** option in the context menu if the user doesn't have any blocks on the workspace. Add this code in `index.js`: ```js -function registerOutputOption() { - const outputOption = { - displayText: 'I have an output connection', +function registerHelpItem() { + const helpItem = { + displayText: 'Help! There are no blocks', preconditionFn: function(scope) { - if (scope.block.outputConnection) { + if (!(scope.focusedNode instanceof Blockly.WorkspaceSvg)) return 'hidden'; + if (!scope.focusedNode.getTopBlocks().length) { return 'enabled'; } return 'hidden'; }, callback: function(scope) { }, - scopeType: Blockly.ContextMenuRegistry.ScopeType.BLOCK, - id: 'block_has_output', + id: 'help_no_blocks', weight: 100, }; - Blockly.ContextMenuRegistry.registry.register(outputOption); + Blockly.ContextMenuRegistry.registry.register(helpItem); } ``` -Don't forget to call `registerHelpOption` and `registerOutputOption` from your `start` function. +Don't forget to call `registerHelpItem` from your `start` function. ### Test it - Reload your page and right-click on the workspace. You should see an option labeled "Help! There are no blocks". - Add a block to the workspace and right-click on the workspace again. The **Help** option should be gone. -- Add a block with an output connection. Right-click the block and confirm that there is an option labeled "I have an output connection". -- Add an if block. Right-click the block and confirm that there is no option labeled "I have an output connection". ## Callback -The callback function determines what happens when you click on the context menu option. Like the precondition, it can use the `scope` argument to access the workspace, block, or comment. +The callback function determines what happens when you click on the context menu option. Like the precondition, it can use the `scope` argument to access the Blockly component on which the context menu was invoked. It is also passed a `PointerEvent` which is the original event that triggered opening the context menu (not the event that selected the current option). This lets you, for example, figure out where on the workspace the context menu was opened so you can create a new element there. @@ -270,7 +238,7 @@ callback: function(scope) { 'fields': { 'TEXT': 'Now there is a block' } - }, scope.workspace); + }, scope.focusedNode); } ``` @@ -289,34 +257,33 @@ So far the `displayText` has always been a simple string, but it can also be HTM When defined as a function `displayText` accepts a `scope` argument, just like `callback` and `preconditionFn`. -As an example, add this context menu option. The display text depends on the block type. +As an example, add this registry item. The display text depends on the block type. ```js -function registerDisplayOption() { - const displayOption = { +function registerDisplayItem() { + const displayItem = { displayText: function(scope) { - if (scope.block.type.startsWith('text')) { + if (scope.focusedNode.type.startsWith('text')) { return 'Text block'; - } else if (scope.block.type.startsWith('controls')) { + } else if (scope.focusedNode.type.startsWith('controls')) { return 'Controls block'; } else { return 'Some other block'; } }, preconditionFn: function(scope) { - return 'enabled'; + return scope.focusedNode instanceof Blockly.BlockSvg ? 'enabled' : 'hidden'; }, callback: function(scope) { }, - scopeType: Blockly.ContextMenuRegistry.ScopeType.BLOCK, id: 'display_text_example', weight: 100, }; - Blockly.ContextMenuRegistry.registry.register(displayOption); + Blockly.ContextMenuRegistry.registry.register(displayItem); } ``` -As usual, remember to call `registerDisplayOption()` from your `start` function. +As usual, remember to call `registerDisplayItem()` from your `start` function. ### Test it @@ -329,25 +296,61 @@ The last two properties of a registry item are `weight` and `id`. ### Weight -The `weight` property is a number that determines the order of the items in the context menu. A higher number means your option will be lower in the list. +The `weight` property is a number that determines the order of the options in the context menu. A higher number means your option will be lower in the list. -Test this by updating the `weight` property on one of your new context menu options and confirming that the item moves to the top or bottom of the list. +Test this by updating the `weight` property on one of your new registry items and confirming that the corresponding option moves to the top or bottom of the list. Note that weight does not have to be positive or integer-valued. ### Id -Every registry item has an `id` that can be used to unregister it. You can use this to get rid of context menu options that you don't want. +Every registry item has an `id` that can be used to unregister it. You can use this to get rid of registry items that you don't want. -For instance, you can remove the option that deletes all blocks on the workspace: +For instance, you can remove the item that deletes all blocks on the workspace: ```js Blockly.ContextMenuRegistry.registry.unregister('workspaceDelete'); ``` -### Default options +### Default items + +For a list of the default registry items that Blockly provides, look at [contextmenu_items.ts](https://github.com/google/blockly/blob/master/core/contextmenu_items.ts). Each entry contains both the `id` and the `weight`. + +## Separators + +You can use separators to break your context menu into different sections. + +Separators differ from other items in two ways: They cannot have `displayText`, `preconditionFn`, or `callback` properties and they can only be scoped with the `scopeType` property. The latter accepts an enum value of `Blockly.ContextMenuRegistry.ScopeType.WORKSPACE`, `Blockly.ContextMenuRegistry.ScopeType.BLOCK`, or `Blockly.ContextMenuRegistry.ScopeType.COMMENT`. + +Use the `weight` property to position the separator. You'll use a weight of `99` to position the separator just above the other options you added, all of which have a weight of `100`. + +You need to add a separate item for each separator: + +```js +function registerSeparators() { + const workspaceSeparator = { + id: 'workspace_separator', + scopeType: Blockly.ContextMenuRegistry.ScopeType.WORKSPACE, + weight: 99, + separator: true, + } + Blockly.ContextMenuRegistry.registry.register(workspaceSeparator); + + const blockSeparator = { + id: 'block_separator', + scopeType: Blockly.ContextMenuRegistry.ScopeType.BLOCK, + weight: 99, + separator: true, + } + Blockly.ContextMenuRegistry.registry.register(blockSeparator); +}; +``` + +As usual, remember to call `registerSeparators()` from your `start` function. + +### Test it -For a list of the default options that Blockly provides, look at [contextmenu_items.ts](https://github.com/google/blockly/blob/master/core/contextmenu_items.ts). Each entry contains both the `id` and the `weight`. +Right-click the workspace and a block and check that the separator line is there. ## Summary diff --git a/examples/context-menu-codelab/complete-code/index.js b/examples/context-menu-codelab/complete-code/index.js index 5c13604ce6..f06cc8aff1 100644 --- a/examples/context-menu-codelab/complete-code/index.js +++ b/examples/context-menu-codelab/complete-code/index.js @@ -3,56 +3,56 @@ let workspace = null; function start() { - registerFirstContextMenuOptions(); - registerOutputOption(); - registerHelpOption(); - registerDisplayOption(); + registerHelloWorldItem(); + registerHelpItem(); + registerDisplayItem(); Blockly.ContextMenuRegistry.registry.unregister('workspaceDelete'); + registerSeparators(); + + Blockly.ContextMenuItems.registerCommentOptions(); // Create main workspace. workspace = Blockly.inject('blocklyDiv', { toolbox: toolboxSimple, }); } -function registerFirstContextMenuOptions() { - // This context menu item shows how to use a precondition function to set the visibility of the item. - const workspaceItem = { +function registerHelloWorldItem() { + const helloWorldItem = { displayText: 'Hello World', - // Precondition: Enable for the first 30 seconds of every minute; disable for the next 30 seconds. preconditionFn: function (scope) { - const now = new Date(Date.now()); - if (now.getSeconds() < 30) { - return 'enabled'; + // Only display this option for workspaces and blocks. + if (scope.focusedNode instanceof Blockly.WorkspaceSvg || + scope.focusedNode instanceof Blockly.BlockSvg) { + // Enable for the first 30 seconds of every minute; disable for the next 30 seconds. + const now = new Date(Date.now()); + if (now.getSeconds() < 30) { + return 'enabled'; + } + return 'disabled'; } - return 'disabled'; + return 'hidden'; }, callback: function (scope) {}, - scopeType: Blockly.ContextMenuRegistry.ScopeType.WORKSPACE, id: 'hello_world', weight: 100, }; // Register. - Blockly.ContextMenuRegistry.registry.register(workspaceItem); - - // Duplicate the workspace item (using the spread operator). - const blockItem = {...workspaceItem}; - // Use block scope and update the id to a nonconflicting value. - blockItem.scopeType = Blockly.ContextMenuRegistry.ScopeType.BLOCK; - blockItem.id = 'hello_world_block'; - Blockly.ContextMenuRegistry.registry.register(blockItem); + Blockly.ContextMenuRegistry.registry.register(helloWorldItem); } -function registerHelpOption() { +function registerHelpItem() { const helpItem = { displayText: 'Help! There are no blocks', - // Use the workspace scope in the precondition function to check for blocks on the workspace. - preconditionFn: function (scope) { - if (!scope.workspace.getTopBlocks().length) { + preconditionFn: function(scope) { + // Only display this option on workspace context menus. + if (!(scope.focusedNode instanceof Blockly.WorkspaceSvg)) return 'hidden'; + // Use the focused node, which is a WorkspaceSvg, to check for blocks on the workspace. + if (!scope.focusedNode.getTopBlocks().length) { return 'enabled'; } return 'hidden'; }, - // Use the workspace scope in the callback function to add a block to the workspace. + // Use the focused node in the callback function to add a block to the workspace. callback: function (scope) { Blockly.serialization.blocks.append( { @@ -61,55 +61,51 @@ function registerHelpOption() { TEXT: 'Now there is a block', }, }, - scope.workspace, + scope.focusedNode, ); }, - scopeType: Blockly.ContextMenuRegistry.ScopeType.WORKSPACE, id: 'help_no_blocks', weight: 100, }; Blockly.ContextMenuRegistry.registry.register(helpItem); } -function registerOutputOption() { - const outputOption = { - displayText: 'I have an output connection', - // Use the block scope in the precondition function to hide the option on blocks with no - // output connection. - preconditionFn: function (scope) { - if (scope.block.outputConnection) { - return 'enabled'; - } - return 'hidden'; - }, - callback: function (scope) {}, - scopeType: Blockly.ContextMenuRegistry.ScopeType.BLOCK, - id: 'block_has_output', - // Use a larger weight to push the option lower on the context menu. - weight: 200, - }; - Blockly.ContextMenuRegistry.registry.register(outputOption); -} - -function registerDisplayOption() { - const displayOption = { - // Use the block scope to set display text dynamically based on the type of the block. - displayText: function (scope) { - if (scope.block.type.startsWith('text')) { +function registerDisplayItem() { + const displayItem = { + // Use the focused node (a BlockSvg) to set display text dynamically based on the type of the block. + displayText: function(scope) { + if (scope.focusedNode.type.startsWith('text')) { return 'Text block'; - } else if (scope.block.type.startsWith('controls')) { + } else if (scope.focusedNode.type.startsWith('controls')) { return 'Controls block'; } else { return 'Some other block'; } }, - preconditionFn: function (scope) { - return 'enabled'; + preconditionFn: function(scope) { + return scope.focusedNode instanceof Blockly.BlockSvg ? 'enabled' : 'hidden'; }, callback: function (scope) {}, - scopeType: Blockly.ContextMenuRegistry.ScopeType.BLOCK, id: 'display_text_example', weight: 100, }; - Blockly.ContextMenuRegistry.registry.register(displayOption); + Blockly.ContextMenuRegistry.registry.register(displayItem); } + +function registerSeparators() { + const workspaceSeparator = { + id: 'workspace_separator', + scopeType: Blockly.ContextMenuRegistry.ScopeType.WORKSPACE, + weight: 99, + separator: true, + } + Blockly.ContextMenuRegistry.registry.register(workspaceSeparator); + + const blockSeparator = { + id: 'block_separator', + scopeType: Blockly.ContextMenuRegistry.ScopeType.BLOCK, + weight: 99, + separator: true, + } + Blockly.ContextMenuRegistry.registry.register(blockSeparator); +}; diff --git a/examples/context-menu-codelab/starter-code/index.js b/examples/context-menu-codelab/starter-code/index.js index e712891f4b..bf58b8165e 100644 --- a/examples/context-menu-codelab/starter-code/index.js +++ b/examples/context-menu-codelab/starter-code/index.js @@ -3,6 +3,7 @@ let workspace = null; function start() { + Blockly.ContextMenuItems.registerCommentOptions(); // Create main workspace. workspace = Blockly.inject('blocklyDiv', { toolbox: toolboxSimple, From a8491f6a84e6fc1f968ce60119e68a00053b72fe Mon Sep 17 00:00:00 2001 From: Ronald Bourret Date: Wed, 22 Oct 2025 21:33:37 +0000 Subject: [PATCH 2/9] docs: Clean up mistakes --- .../context_menu_option.md | 40 +++++++++---------- .../complete-code/index.js | 2 +- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/codelabs/context_menu_option/context_menu_option.md b/codelabs/context_menu_option/context_menu_option.md index 4622232db7..7b900828eb 100644 --- a/codelabs/context_menu_option/context_menu_option.md +++ b/codelabs/context_menu_option/context_menu_option.md @@ -81,16 +81,16 @@ Add a function to `index.js` named `registerHelloWorldItem`. Create a new regist ```js function registerHelloWorldItem() { - const helloWorldItem = { - displayText: 'Hello World', - preconditionFn: function(scope) { - return 'enabled'; - }, - callback: function(scope) { - }, - id: 'hello_world', - weight: 100, - }; + const helloWorldItem = { + displayText: 'Hello World', + preconditionFn: function(scope) { + return 'enabled'; + }, + callback: function(scope) { + }, + id: 'hello_world', + weight: 100, + }; } ``` @@ -232,14 +232,14 @@ It is also passed a `PointerEvent` which is the original event that triggered op As an example, update the help item's `callback` to add a block to the workspace when clicked: ```js -callback: function(scope) { - Blockly.serialization.blocks.append({ - 'type': 'text', - 'fields': { - 'TEXT': 'Now there is a block' - } - }, scope.focusedNode); -} + callback: function(scope) { + Blockly.serialization.blocks.append({ + 'type': 'text', + 'fields': { + 'TEXT': 'Now there is a block' + } + }, scope.focusedNode); + }, ``` ### Test it @@ -309,7 +309,7 @@ Every registry item has an `id` that can be used to unregister it. You can use t For instance, you can remove the item that deletes all blocks on the workspace: ```js -Blockly.ContextMenuRegistry.registry.unregister('workspaceDelete'); + Blockly.ContextMenuRegistry.registry.unregister('workspaceDelete'); ``` ### Default items @@ -343,7 +343,7 @@ function registerSeparators() { separator: true, } Blockly.ContextMenuRegistry.registry.register(blockSeparator); -}; +} ``` As usual, remember to call `registerSeparators()` from your `start` function. diff --git a/examples/context-menu-codelab/complete-code/index.js b/examples/context-menu-codelab/complete-code/index.js index f06cc8aff1..e9a3c542d9 100644 --- a/examples/context-menu-codelab/complete-code/index.js +++ b/examples/context-menu-codelab/complete-code/index.js @@ -108,4 +108,4 @@ function registerSeparators() { separator: true, } Blockly.ContextMenuRegistry.registry.register(blockSeparator); -}; +} From e575c23313c98378e2b409f926ae5137846a4d00 Mon Sep 17 00:00:00 2001 From: Ronald Bourret Date: Thu, 23 Oct 2025 16:59:48 +0000 Subject: [PATCH 3/9] docs: Run Prettier --- .../complete-code/index.js | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/examples/context-menu-codelab/complete-code/index.js b/examples/context-menu-codelab/complete-code/index.js index e9a3c542d9..8d192656bb 100644 --- a/examples/context-menu-codelab/complete-code/index.js +++ b/examples/context-menu-codelab/complete-code/index.js @@ -21,8 +21,10 @@ function registerHelloWorldItem() { displayText: 'Hello World', preconditionFn: function (scope) { // Only display this option for workspaces and blocks. - if (scope.focusedNode instanceof Blockly.WorkspaceSvg || - scope.focusedNode instanceof Blockly.BlockSvg) { + if ( + scope.focusedNode instanceof Blockly.WorkspaceSvg || + scope.focusedNode instanceof Blockly.BlockSvg + ) { // Enable for the first 30 seconds of every minute; disable for the next 30 seconds. const now = new Date(Date.now()); if (now.getSeconds() < 30) { @@ -43,7 +45,7 @@ function registerHelloWorldItem() { function registerHelpItem() { const helpItem = { displayText: 'Help! There are no blocks', - preconditionFn: function(scope) { + preconditionFn: function (scope) { // Only display this option on workspace context menus. if (!(scope.focusedNode instanceof Blockly.WorkspaceSvg)) return 'hidden'; // Use the focused node, which is a WorkspaceSvg, to check for blocks on the workspace. @@ -73,7 +75,7 @@ function registerHelpItem() { function registerDisplayItem() { const displayItem = { // Use the focused node (a BlockSvg) to set display text dynamically based on the type of the block. - displayText: function(scope) { + displayText: function (scope) { if (scope.focusedNode.type.startsWith('text')) { return 'Text block'; } else if (scope.focusedNode.type.startsWith('controls')) { @@ -82,8 +84,10 @@ function registerDisplayItem() { return 'Some other block'; } }, - preconditionFn: function(scope) { - return scope.focusedNode instanceof Blockly.BlockSvg ? 'enabled' : 'hidden'; + preconditionFn: function (scope) { + return scope.focusedNode instanceof Blockly.BlockSvg + ? 'enabled' + : 'hidden'; }, callback: function (scope) {}, id: 'display_text_example', @@ -98,7 +102,7 @@ function registerSeparators() { scopeType: Blockly.ContextMenuRegistry.ScopeType.WORKSPACE, weight: 99, separator: true, - } + }; Blockly.ContextMenuRegistry.registry.register(workspaceSeparator); const blockSeparator = { @@ -106,6 +110,6 @@ function registerSeparators() { scopeType: Blockly.ContextMenuRegistry.ScopeType.BLOCK, weight: 99, separator: true, - } + }; Blockly.ContextMenuRegistry.registry.register(blockSeparator); } From 70b47eab2b5df073a073ca36cb4b84f8c3d63830 Mon Sep 17 00:00:00 2001 From: Ronald Bourret Date: Thu, 23 Oct 2025 18:54:54 +0000 Subject: [PATCH 4/9] docs: Apply Prettier changes to doc --- codelabs/context_menu_option/context_menu_option.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/codelabs/context_menu_option/context_menu_option.md b/codelabs/context_menu_option/context_menu_option.md index 7b900828eb..3c184927ce 100644 --- a/codelabs/context_menu_option/context_menu_option.md +++ b/codelabs/context_menu_option/context_menu_option.md @@ -174,8 +174,10 @@ Use of the `preconditionFn` is not limited to checking the type of the Blockly c ```js preconditionFn: function (scope) { - if (scope.focusedNode instanceof Blockly.WorkspaceSvg || - scope.focusedNode instanceof Blockly.BlockSvg) { + if ( + scope.focusedNode instanceof Blockly.WorkspaceSvg || + scope.focusedNode instanceof Blockly.BlockSvg + ) { const now = new Date(Date.now()); if (now.getSeconds() < 30) { return 'enabled'; @@ -272,7 +274,9 @@ function registerDisplayItem() { } }, preconditionFn: function(scope) { - return scope.focusedNode instanceof Blockly.BlockSvg ? 'enabled' : 'hidden'; + return scope.focusedNode instanceof Blockly.BlockSvg + ? 'enabled' + : 'hidden'; }, callback: function(scope) { }, From 4e90d5eadc70be640f9617f53229b4e6f8995087 Mon Sep 17 00:00:00 2001 From: Ronald Bourret Date: Thu, 23 Oct 2025 19:01:44 +0000 Subject: [PATCH 5/9] docs: One more Prettier change to doc --- codelabs/context_menu_option/context_menu_option.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/codelabs/context_menu_option/context_menu_option.md b/codelabs/context_menu_option/context_menu_option.md index 3c184927ce..cabff72363 100644 --- a/codelabs/context_menu_option/context_menu_option.md +++ b/codelabs/context_menu_option/context_menu_option.md @@ -152,8 +152,10 @@ You can now test `scope.focusedNode` to display the "Hello World" option in work const helloWorldItem = { ... preconditionFn: function (scope) { - if (scope.focusedNode instanceof Blockly.WorkspaceSvg || - scope.focusedNode instanceof Blockly.BlockSvg) { + if ( + scope.focusedNode instanceof Blockly.WorkspaceSvg || + scope.focusedNode instanceof Blockly.BlockSvg + ) { return 'enabled'; } return 'hidden'; From b80011147bd25168cebb4facb2586c4c54251f3f Mon Sep 17 00:00:00 2001 From: Ronald Bourret Date: Fri, 24 Oct 2025 19:48:06 +0000 Subject: [PATCH 6/9] Replace 'click' with 'open' or 'select' --- .../context_menu_option.md | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/codelabs/context_menu_option/context_menu_option.md b/codelabs/context_menu_option/context_menu_option.md index cabff72363..c22857a84d 100644 --- a/codelabs/context_menu_option/context_menu_option.md +++ b/codelabs/context_menu_option/context_menu_option.md @@ -15,7 +15,7 @@ In this codelab you will learn how to: - Add a context menu option to the workspace. - Add a context menu option to all blocks. - Use precondition functions to hide or disable context menu options. -- Take an action when a menu option is clicked. +- Take an action when a menu option is selected. - Customize ordering and display text for context menu options. ### What you'll build @@ -58,17 +58,17 @@ To run the code, simple open `starter-code/index.html` in a browser. You should ## Add a context menu item -In this section you will create a very basic `Blockly.ContextMenuRegistry.RegistryItem`, then register it to display when you right-click on the workspace, a block, or a comment. +In this section you will create a very basic `Blockly.ContextMenuRegistry.RegistryItem`, then register it to display when you open a context menu on the workspace, a block, or a comment. ### The RegistryItem -A context menu consists of one or more menu options that a user can click. Blockly stores information about menu option as items in a registry. You can think of the _registry items_ as templates for constructing _menu options_. When the user right-clicks, Blockly retrieves all of the registry items that apply to the current context and uses them to construct a list of menu options. +A context menu consists of one or more menu options that a user can select. Blockly stores information about menu option as items in a registry. You can think of the _registry items_ as templates for constructing _menu options_. When the user opens a context menu, Blockly retrieves all of the registry items that apply to the current context and uses them to construct a list of menu options. Each item in the registry has several properties: - `displayText`: The text to show in the menu. Either a string, or HTML, or a function that returns either of the former. - `preconditionFn`: Function that returns one of `'enabled'`, `'disabled'`, or `'hidden'` to determine whether and how the menu option should be rendered. -- `callback`: A function called when the menu option is clicked. +- `callback`: A function called when the menu option is selected. - `id`: A unique string id for the item. - `weight`: A number that determines the sort order of the option. Options with higher weights appear later in the context menu. @@ -126,11 +126,11 @@ Note: you will never need to make a new `ContextMenuRegistry`. Always use the si ### Test it -Reload your web page and right-click on the workspace. You should see a new option labeled "Hello World" at the bottom of the context menu. +Reload your web page and open a context menu on the workspace (right-click with a mouse, or press `Ctrl+Enter` (Windows) or `Command+Enter` (Mac) if you are navigating Blockly with the keyboard). You should see a new option labeled "Hello World" at the bottom of the context menu. ![A context menu. The last option says "Hello World".](hello_world.png) -Next, drag a block onto the workspace and right-click on the block. You'll see "Hello World" at the bottom of the block's context menu. Finally, right-click on the workspace and create a comment, then right-click on the comment's header. "Hello World" should be at the bottom of the context menu. +Next, drag a block onto the workspace and open a context menu on the block. You'll see "Hello World" at the bottom of the block's context menu. Finally, open a context menu on the workspace and create a comment, then open a context menu on the comment's header. "Hello World" should be at the bottom of the context menu. ## Precondition: Node type @@ -138,11 +138,11 @@ Each registry item has a `preconditionFn`. It is called by Blockly to decide whe ### The scope argument -The `scope` argument is an object that is passed to `preconditionFn`. You'll use the `scope.focusedNode` property to determine which object the context menu was invoked on. Why a focused node? Because all Blockly components that support context menus implement the `IFocusableNode` interface, so it's a handy way to pass objects (like workspaces, blocks, and comments) that otherwise have nothing in common. +The `scope` argument is an object that is passed to `preconditionFn`. You'll use the `scope.focusedNode` property to determine which object the context menu was invoked on. Why a focused node? Why a focused node? Because all Blockly components that support context menus implement the `IFocusableNode` interface, so it's a handy way to pass objects (like workspaces, blocks, and comments) that otherwise have nothing in common. ### Return value -The return value of `preconditionFn` is `'enabled'`, `'disabled'`, or `'hidden'`. An **enabled** option is shown with black text and is clickable. A **disabled** option is shown with grey text and is not clickable. A **hidden** option is not included in the context menu at all. +The return value of `preconditionFn` is `'enabled'`, `'disabled'`, or `'hidden'`. An **enabled** option is shown with black text and is selectable. A **disabled** option is shown with grey text and is not selectable. A **hidden** option is not included in the context menu at all. ### Write the function @@ -166,7 +166,7 @@ You can now test `scope.focusedNode` to display the "Hello World" option in work ### Test it -Right-click the workspace, a block, and a comment. You should see a "Hello World" option on the workspace and block context menus, but not on the comment context menu. +Open a context menu on the workspace, a block, and a comment. You should see a "Hello World" option on the workspace and block context menus, but not on the comment context menu. ![An if block with a context menu with five options. The last option says "Hello World".](hello_world_block.png) @@ -192,7 +192,7 @@ Use of the `preconditionFn` is not limited to checking the type of the Blockly c ### Test it -Reload your workspace, check your watch, and right-click on the workspace to confirm the timing. The option will always be in the menu, but will sometimes be greyed out. +Reload your workspace, check your watch, and open a context menu on the workspace to confirm the timing. The option will always be in the menu, but will sometimes be greyed out. ![A context menu. The last option says "Hello World" but the text is grey, indicating that it cannot be selected.](hello_world_grey.png) @@ -224,16 +224,16 @@ Don't forget to call `registerHelpItem` from your `start` function. ### Test it -- Reload your page and right-click on the workspace. You should see an option labeled "Help! There are no blocks". -- Add a block to the workspace and right-click on the workspace again. The **Help** option should be gone. +- Reload your page and open a context menu on the workspace. You should see an option labeled "Help! There are no blocks". +- Add a block to the workspace and open a context menu on the workspace again. The **Help** option should be gone. ## Callback -The callback function determines what happens when you click on the context menu option. Like the precondition, it can use the `scope` argument to access the Blockly component on which the context menu was invoked. +The callback function determines what happens when you select the context menu option. Like the precondition, it can use the `scope` argument to access the Blockly component on which the context menu was invoked. It is also passed a `PointerEvent` which is the original event that triggered opening the context menu (not the event that selected the current option). This lets you, for example, figure out where on the workspace the context menu was opened so you can create a new element there. -As an example, update the help item's `callback` to add a block to the workspace when clicked: +As an example, update the help item's `callback` to add a block to the workspace when selected: ```js callback: function(scope) { @@ -248,8 +248,8 @@ As an example, update the help item's `callback` to add a block to the workspace ### Test it -- Reload the page and right-click on the workspace. -- Click the **Help** option. +- Reload the page and open a context menu on the workspace. +- Select the **Help** option. - A text block should appear in the top left of the workspace. @@ -293,7 +293,7 @@ As usual, remember to call `registerDisplayItem()` from your `start` function. ### Test it -- Reload the workspace and right-click on various blocks. +- Reload the workspace and open context menus on various blocks. - The last context menu option's text should vary based on the block type. ## Weight and id @@ -356,7 +356,7 @@ As usual, remember to call `registerSeparators()` from your `start` function. ### Test it -Right-click the workspace and a block and check that the separator line is there. +Open a context menu on the workspace and a block and check that the separator line is there. ## Summary From fc84c75d77bc4ca63c9a80554a171f293064eda7 Mon Sep 17 00:00:00 2001 From: Ronald Bourret Date: Fri, 24 Oct 2025 19:51:01 +0000 Subject: [PATCH 7/9] Clarify how to restrict scope --- codelabs/context_menu_option/context_menu_option.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/codelabs/context_menu_option/context_menu_option.md b/codelabs/context_menu_option/context_menu_option.md index c22857a84d..904f39d1f6 100644 --- a/codelabs/context_menu_option/context_menu_option.md +++ b/codelabs/context_menu_option/context_menu_option.md @@ -164,6 +164,8 @@ You can now test `scope.focusedNode` to display the "Hello World" option in work }; ``` +Notice that the code tests for where context menus are allowed, rather than where they are not allowed. This is because custom code (such as a plugin) can add context menus to any Blockly component that can be focused. Thus, testing for what something isn't may result in allowing context menus on more components than you anticipated. + ### Test it Open a context menu on the workspace, a block, and a comment. You should see a "Hello World" option on the workspace and block context menus, but not on the comment context menu. From c7793d9c0840b5e677c78c1070672d821b611658 Mon Sep 17 00:00:00 2001 From: Ronald Bourret Date: Fri, 24 Oct 2025 20:03:16 +0000 Subject: [PATCH 8/9] Clarify focusedNode --- codelabs/context_menu_option/context_menu_option.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codelabs/context_menu_option/context_menu_option.md b/codelabs/context_menu_option/context_menu_option.md index 904f39d1f6..b413db2826 100644 --- a/codelabs/context_menu_option/context_menu_option.md +++ b/codelabs/context_menu_option/context_menu_option.md @@ -138,7 +138,7 @@ Each registry item has a `preconditionFn`. It is called by Blockly to decide whe ### The scope argument -The `scope` argument is an object that is passed to `preconditionFn`. You'll use the `scope.focusedNode` property to determine which object the context menu was invoked on. Why a focused node? Why a focused node? Because all Blockly components that support context menus implement the `IFocusableNode` interface, so it's a handy way to pass objects (like workspaces, blocks, and comments) that otherwise have nothing in common. +The `scope` argument is an object that is passed to `preconditionFn`. You'll use the `scope.focusedNode` property to determine which object the context menu was invoked on. Why a focused node? Because Blockly keeps track of where the user is -- that is, what node (component) the user is focused on -- and opens the context menu on that node. ### Return value From 3c2cbf548fb4f856cb363d5d682c0f8208839635 Mon Sep 17 00:00:00 2001 From: Ronald Bourret Date: Fri, 24 Oct 2025 23:46:33 +0000 Subject: [PATCH 9/9] Clean up wording --- codelabs/context_menu_option/context_menu_option.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codelabs/context_menu_option/context_menu_option.md b/codelabs/context_menu_option/context_menu_option.md index b413db2826..81e6e2a088 100644 --- a/codelabs/context_menu_option/context_menu_option.md +++ b/codelabs/context_menu_option/context_menu_option.md @@ -164,7 +164,7 @@ You can now test `scope.focusedNode` to display the "Hello World" option in work }; ``` -Notice that the code tests for where context menus are allowed, rather than where they are not allowed. This is because custom code (such as a plugin) can add context menus to any Blockly component that can be focused. Thus, testing for what something isn't may result in allowing context menus on more components than you anticipated. +Notice that the code tests for where context menus are allowed, rather than where they are not allowed. This is because custom code (such as a plugin) can add context menus to any Blockly component that can be focused. Thus, testing for specific types rather than allowing all (or all but certain types) ensures that context menus are not shown on more components than you anticipated. ### Test it