Skip to main content

Implement the Context Menu Item

We now have a button in the context menu but when we click it nothing happens.

To respond to a click on our menu item edit the onClick callback in the contextMenu.js file.

contextMenu.js
onClick(context) {
const initiative = window.prompt("Enter the initiative value");
OBR.scene.items.updateItems(context.items, (items) => {
for (let item of items) {
item.metadata[`${ID}/metadata`] = {
initiative,
};
}
});
},

To make things easier we first use window.prompt to get user input.

We then use the updateItems function in the Scene API to update the selected items.

For each item selected we add a value to the its metadata. To avoid collisions with other extension we make sure we prefix our value in the metadata with our ID.

The metadata field on an item allows you to store custom data with that item. The data will also be saved with the scene which means the metadata will be synced with every player who is connected to the room.

Removing a Character From Initiative

We now have a context menu item that will add an initiative value to a character. In order to to remove a character from initiative let's update our setupContextMenu function with a new icon.

contextMenu.js
icons: [
{
icon: "/add.svg",
label: "Add to Initiative",
filter: {
every: [
{ key: "layer", value: "CHARACTER" },
{ key: ["metadata", `${ID}/metadata`], value: undefined },
],
},
},
{
icon: "/remove.svg",
label: "Remove from Initiative",
filter: {
every: [{ key: "layer", value: "CHARACTER" }],
},
},
],

Above we add the remove.svg icon with a label Remove from Initiative. In order to show the new icon we've also added a new filter to the add.svg icon. The filter checks for the existence of our custom metadata property. If we don't have any initiative yet then we show the add.svg icon otherwise we show the remove.svg icon.

To learn more about how filters work with context menus see here.

Now that we've added our new icon we need to update the onClick handler to remove our metadata when it is clicked.

contextMenu.js
onClick(context) {
const addToInitiative = context.items.every(
(item) => item.metadata[`${ID}/metadata`] === undefined
);
if (addToInitiative) {
const initiative = window.prompt("Enter the initiative value");
OBR.scene.items.updateItems(context.items, (items) => {
for (let item of items) {
item.metadata[`${ID}/metadata`] = {
initiative,
};
}
});
} else {
OBR.scene.items.updateItems(context.items, (items) => {
for (let item of items) {
delete item.metadata[`${ID}/metadata`]
}
});
}
},

We first check our selected items to see whether we should add or remove the initiative value. Lastly to remove the initiative value we delete our custom field in the metadata object.

Back in Owlbear Rodeo you will now be able to click the Add to Initiative context menu item. Fill in an initiative value then remove that item if needed.

The full source of the contextMenu.js file should now look like this:

contextMenu.js
import OBR from "@owlbear-rodeo/sdk";

const ID = "com.tutorial.initiative-tracker";

export function setupContextMenu() {
OBR.contextMenu.create({
id: `${ID}/context-menu`,
icons: [
{
icon: "/add.svg",
label: "Add to Initiative",
filter: {
every: [
{ key: "layer", value: "CHARACTER" },
{ key: ["metadata", `${ID}/metadata`], value: undefined },
],
},
},
{
icon: "/remove.svg",
label: "Remove from Initiative",
filter: {
every: [{ key: "layer", value: "CHARACTER" }],
},
},
],
onClick(context) {
const addToInitiative = context.items.every(
(item) => item.metadata[`${ID}/metadata`] === undefined
);
if (addToInitiative) {
const initiative = window.prompt("Enter the initiative value");
OBR.scene.items.updateItems(context.items, (items) => {
for (let item of items) {
item.metadata[`${ID}/metadata`] = {
initiative,
};
}
});
} else {
OBR.scene.items.updateItems(context.items, (items) => {
for (let item of items) {
delete item.metadata[`${ID}/metadata`];
}
});
}
},
});
}