In this codelab you will learn how to:
A very simple Blockly workspace with a few new context menu options.
This codelab is focused on Blockly's context menus. Non-relevant concepts and code are glossed over and are provided for you to simply copy and paste.
You can get the sample code for this code by either downloading the zip here:
or by cloning this git repo:
git clone https://github.com/google/blockly-samples.git
If you downloaded the source as a zip, unpacking it should give you a root folder named blockly-samples-master
.
The relevant files are in examples/context-menu-codelab
. There are two versions of the app:
starter-code/
: The starter code that you'll build upon in this codelab.complete-code/
: The code after completing the codelab, in case you get lost or want to compare to your version.Each folder contains:
index.js
- The codelab's logic. To start, it just injects a simple workspace.index.html
- A web page containing a simple blockly workspace.To run the code, simple open starter-code/index.html
in a browser. You should see a Blockly workspace with an always-open flyout.
In this section you will create a very basic Blockly.ContextMenuRegistry.RegistryItem
, then register it to display when you right-click on the workspace.
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.
Each menu option 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.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.
Add a function to index.js
named registerFirstContextMenuOptions
. Create a new registry item in your function:
function registerFirstContextMenuOptions() {
const workspaceItem = {
displayText: 'Hello World',
preconditionFn: function(scope) {
return 'enabled';
},
callback: function(scope) {
},
scopeType: Blockly.ContextMenuRegistry.ScopeType.WORKSPACE,
id: 'hello_world',
weight: 100,
};
}
Call your function from start
:
function start() {
registerFirstContextMenuOptions();
// Create main workspace.
workspace = Blockly.inject('blocklyDiv',
{
toolbox: toolboxSimple,
});
}
Next, register your item with Blockly:
function registerFirstContextMenuOptions() {
const workspaceItem = {
// ...
};
Blockly.ContextMenuRegistry.registry.register(workspaceItem);
}
Note: you will never need to make a new ContextMenuRegistry
. Always use the singleton Blockly.ContextMenuRegistry.registry
.
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.
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:
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.
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:
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.
Drag a block into the workspace and right-click it. You should see a "Hello world" option on the block context menu.
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 return value should be one of '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.
For instance, let's disable workspaceItem
for the second half of every minute:
preconditionFn: function(scope) {
const now = new Date(Date.now());
if (now.getSeconds() < 30) {
return 'enabled';
}
return 'disabled';
}
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.
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.
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:
WORKSPACE
scope type you can access the workspace
property, which is an instance of Blockly.WorkspaceSvg
.BLOCK
scope type you can access the block
property, which is an instance of Blockly.BlockSvg
.COMMENT
scope type you can access the comment
property, which is an instance of Blockly.RenderedWorkspaceComment
.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
:
function registerHelpOption() {
const helpItem = {
displayText: 'Help! There are no blocks',
preconditionFn: function(scope) {
if (!scope.workspace.getTopBlocks().length) {
return 'enabled';
}
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.
To demonstrate block scope, add an option that is only visible when the block has an output connection:
function registerOutputOption() {
const outputOption = {
displayText: 'I have an 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',
weight: 100,
};
Blockly.ContextMenuRegistry.registry.register(outputOption);
}
Don't forget to call registerHelpOption
and registerOutputOption
from your start
function.
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.
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:
callback: function(scope) {
Blockly.serialization.blocks.append({
'type': 'text',
'fields': {
'TEXT': 'Now there is a block'
}
}, scope.workspace);
}
So far the displayText
has always been a simple string, but it can also be HTML, or a function that returns either of the former. Using a function can be useful when you want a context-dependent message.
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.
function registerDisplayOption() {
const displayOption = {
displayText: function(scope) {
if (scope.block.type.startsWith('text')) {
return 'Text block';
} else if (scope.block.type.startsWith('controls')) {
return 'Controls block';
} else {
return 'Some other block';
}
},
preconditionFn: function(scope) {
return 'enabled';
},
callback: function(scope) {
},
scopeType: Blockly.ContextMenuRegistry.ScopeType.BLOCK,
id: 'display_text_example',
weight: 100,
};
Blockly.ContextMenuRegistry.registry.register(displayOption);
}
As usual, remember to call registerDisplayOption()
from your start
function.
The last two properties of a registry item are weight
and id
.
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.
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.
Note that weight does not have to be positive or integer-valued.
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.
For instance, you can remove the option that deletes all blocks on the workspace:
Blockly.ContextMenuRegistry.registry.unregister('workspaceDelete');
For a list of the default options that Blockly provides, look at contextmenu_items.ts. Each entry contains both the id
and the weight
.
In this codelab you have learned how to create and modify context menu options. You have learned about scope, preconditions, callbacks, and display text.