Skip to content

Commit

Permalink
Add right-click context menu for ADV/DIS rolls
Browse files Browse the repository at this point in the history
  • Loading branch information
haste committed Dec 19, 2024
1 parent 0f47797 commit 10abcf7
Show file tree
Hide file tree
Showing 10 changed files with 268 additions and 25 deletions.
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
# Changelog

## 0.16.4 (Unreleased)
## 0.17.0 (Unreleased)

### Bug fixes

* Dice buttons will no longer be added to item names in the sidebar heading.

### Enhancements

* It's now possible to right-click dice buttons to roll with Advantage or Disadvantage.

## 0.16.3 (2024-10-25)

### Bug fixes
Expand Down
2 changes: 2 additions & 0 deletions src/characters.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { injectContextMenu } from "~/contextmenu";
import svgLogo from "~/icons/icon.svg";
import { customMod } from "~/mods";
import { injectThemeStyle } from "~/themes";
Expand Down Expand Up @@ -239,6 +240,7 @@ export const characterAppWatcher = (showOptionsButton = false) => {

getCharacterAbilities();
injectThemeStyle();
await injectContextMenu();

observer.disconnect();

Expand Down
2 changes: 1 addition & 1 deletion src/clients/browser/manifest.shared.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"matches": ["*://*.dndbeyond.com/characters/*"],
"run_at": "document_end",
"js": ["characters.js"],
"css": ["css/dialog.css", "css/styles.css"]
"css": ["css/dialog.css", "css/styles.css", "css/contextmenu.css"]
},
{
"matches": ["*://*.dndbeyond.com/monsters*"],
Expand Down
1 change: 1 addition & 0 deletions src/clients/symbiote/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"fonts",
"/characters.js",
"/css/dialog.css",
"/css/contextmenu.css",
"/css/styles.css"
]
}
Expand Down
124 changes: 124 additions & 0 deletions src/contextmenu.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import svgLogo from "~/icons/icon.svg";
import { getOptions } from "~/utils/storage";
import { triggerTalespire } from "~/utils/talespire";

const removeAllMenus = () => {
for (const activeMenu of document.querySelectorAll(
".tales-beyond-extension-contextmenu",
)) {
activeMenu.remove();
}

window.removeEventListener("click", detectLightDismiss);
window.removeEventListener("scroll", removeAllMenus);
};

const detectLightDismiss = (event) => {
const activeMenu = Array.from(
document.querySelectorAll(".tales-beyond-extension-contextmenu"),
);

const clickedMenu = activeMenu.some((elem) => elem.contains(event.target));
if (!clickedMenu) {
removeAllMenus();
}
};

const setupListeners = (button, contextmenu) => {
const label = button.dataset.tsLabel;
const dice = button.dataset.tsDice;

const action = (labelSuffix) => () => {
if (labelSuffix) {
const name = label ? `${label} (${labelSuffix})` : labelSuffix;
triggerTalespire(name, dice, true);
} else {
triggerTalespire(label, dice);
}

removeAllMenus();
};

const [adv, flat, dis] = contextmenu.querySelectorAll(".item");
adv.addEventListener("click", action("ADV"));
flat.addEventListener("click", action());
dis.addEventListener("click", action("DIS"));

window.addEventListener("click", detectLightDismiss, {
capture: true,
passive: true,
});

window.addEventListener("scroll", removeAllMenus, {
capture: true,
passive: true,
});
};

const positionMenu = (button, contextmenu) => {
const menu = contextmenu.querySelector(".menu");
const arrow = contextmenu.querySelector(" .arrow");

const buttonRect = button.getBoundingClientRect();
const menuRect = menu.getBoundingClientRect();

const top =
(buttonRect.top + buttonRect.bottom) / 2 -
menuRect.height / 2 +
window.scrollY;

menu.style.top = `${top}px`;

// Avoid end of screen
if (window.innerWidth < buttonRect.right + menuRect.width + 10) {
arrow.classList.add("right");
menu.style.left = `${buttonRect.left - menuRect.right - 10}px`;
} else {
arrow.classList.add("left");
menu.style.left = `${buttonRect.right - menuRect.left + 10}px`;
}
};

const contextMenu = (event) => {
const diceButton = event.target.closest(".integrated-dice__container");
if (!diceButton) {
return;
}

event.preventDefault();

removeAllMenus();

const contextmenu = document.createElement("div");
contextmenu.classList.add("tales-beyond-extension-contextmenu");
contextmenu.innerHTML = `
<div class="menu">
<div class="arrow"></div>
<h2>
<img>
Tales Beyond
</h2>
<hr />
<div class="item advantage">Advantage</div>
<div class="item">Normal</div>
<div class="item disadvantage">Disadvantage</div>
</div>
`;
const img = contextmenu.querySelector("img");
img.src = svgLogo;

document.body.appendChild(contextmenu);
positionMenu(diceButton, contextmenu);
setupListeners(diceButton, contextmenu);
};

export const injectContextMenu = async () => {
const settings = await getOptions();
if (settings.contextMenuEnabled) {
window.addEventListener("contextmenu", contextMenu);
} else {
window.removeEventListener("contextmenu", contextMenu);
}
};
69 changes: 69 additions & 0 deletions src/css/contextmenu.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
.tales-beyond-extension-contextmenu .arrow {
position: absolute;
width: 0;
height: 0;
border-top: 10px solid transparent;
border-bottom: 10px solid transparent;
top: calc(50% - 10px);
}

.tales-beyond-extension-contextmenu .arrow.left {
border-right: 10px solid #27272c;
left: -10px;
}

.tales-beyond-extension-contextmenu .arrow.right {
border-left: 10px solid #27272c;
right: -10px;
}

.tales-beyond-extension-contextmenu .menu {
position: absolute;
z-index: 2000;
background: #27272c;
padding: 6px 8px;
color: #c5c5ce;
border-radius: .5em;
box-shadow: 0 .5rem 1rem rgba(0, 0, 0, 0.3);
text-align: center;
font-size: 12px;
min-width: max-content;
/* Required because of Popover API */
inset: 0px auto auto 0px;
margin: 0;
border: none;
}

.tales-beyond-extension-contextmenu .item {
padding: 6px 8px;
margin: 2px;
line-height: 18px;
}

.tales-beyond-extension-contextmenu .item:hover {
background-color: #ab79d6;
color: #351d4a;
cursor: pointer;
}

.tales-beyond-extension-contextmenu .item.advantage:not(:hover) {
text-decoration: green wavy underline;
}

.tales-beyond-extension-contextmenu .item.disadvantage:not(:hover) {
text-decoration: red wavy underline;
}

.tales-beyond-extension-contextmenu h2 {
color: #ab79d6;
margin: 0rem 0 0 0;
display: flex;
gap: .5rem;
line-height: 1.2;
align-items: center;
font-size: 1rem;
}

.tales-beyond-extension-contextmenu h2 img {
width: 1.5rem;
}
5 changes: 5 additions & 0 deletions src/options.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ <h1>
Tales Beyond
</h1>

<section id="general">
<h4>General</h4>
<div id="general-list"></div>
</section>

<section id="modifiers">
<h4>Modifier key behavior</h4>
<div id="key-list"></div>
Expand Down
33 changes: 33 additions & 0 deletions src/options.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import { mods } from "~/mods";
import { getOptions, saveOption } from "~/utils/storage";

const general = [
{
id: "contextMenuEnabled",
header: "Right-click menu",
description:
"Open a menu when right-clicking dice buttons to roll with Advantage or Disadvantage.",
},
];

const keys = [
{
id: "modifierKeyShift",
Expand Down Expand Up @@ -72,6 +81,30 @@ const restoreOptions = async () => {

modList.append(option);
}

const generalList = document.querySelector("#general-list");
for (const opt of general) {
// Re-use mod template
const option = modTemplate.content.cloneNode(true);
option.querySelector("label").setAttribute("for", opt.id);

const input = option.querySelector("input");
input.addEventListener("change", (event) =>
saveOption(opt.id, event.target.checked),
);
input.setAttribute("id", opt.id);
if (settings[opt.id]) {
input.setAttribute("checked", "");
}

const description = option.querySelector(".description");
description.firstElementChild.textContent = opt.header;

const text = document.createTextNode(opt.description);
description.appendChild(text);

generalList.append(option);
}
};

document.addEventListener("readystatechange", (event) => {
Expand Down
1 change: 1 addition & 0 deletions src/utils/storage.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const VERSION = 1;
const defaultOptions = {
version: VERSION,
contextMenuEnabled: true,
modifierKeyAlt: "adv-dis",
modifierKeyCtrl: "adv-dis",
modifierKeyShift: "adv-dis",
Expand Down
50 changes: 27 additions & 23 deletions src/utils/talespire.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,30 @@ const checkModifierKeys = (event, name) => {
return modifierAction(action, name);
};

export const triggerTalespire = (name, dice, extraDice) => {
dice = dice.replace(/d100/g, "d100+d10");

let uri;
if (typeof name === "string") {
uri = `talespire://dice/${encodeURIComponent(name)}:${dice}${extraDice ? `/${dice}` : ""}`;
} else {
uri = `talespire://dice/${dice}${extraDice ? `/${dice}` : ""}`;
}

if (TB_DRY_RUN_TALESPIRE_LINKS === "true") {
// biome-ignore lint/suspicious/noConsoleLog: Used during dev only
console.log("TaleSpire Link", { name, dice, extraDice, uri });
} else if (typeof TS !== "undefined" && TS.dice) {
const rollDescriptors = [{ name: name ?? "", roll: dice }];
if (extraDice) {
rollDescriptors.push({ name: "", roll: dice });
}
TS.dice.putDiceInTray(rollDescriptors);
} else {
window.open(uri, "_self");
}
};

export const talespireLink = (elem, label, dice, diceLabel) => {
label = label?.trim();

Expand All @@ -39,33 +63,13 @@ export const talespireLink = (elem, label, dice, diceLabel) => {
link.classList.add("tales-beyond-extension");
link.dataset.tsLabel = label;
link.dataset.tsDice = dice;
link.onclick = (event) => {
link.addEventListener("click", (event) => {
event.stopPropagation();

dice = dice.replace(/d100/g, "d100+d10");

const { name, extraDice } = checkModifierKeys(event, label);

let uri;
if (typeof name === "string") {
uri = `talespire://dice/${encodeURIComponent(name)}:${dice}${extraDice ? `/${dice}` : ""}`;
} else {
uri = `talespire://dice/${dice}${extraDice ? `/${dice}` : ""}`;
}

if (TB_DRY_RUN_TALESPIRE_LINKS === "true") {
// biome-ignore lint/suspicious/noConsoleLog: Used during dev only
console.log("TaleSpire Link", { name, dice, extraDice, uri });
} else if (typeof TS !== "undefined" && TS.dice) {
const rollDescriptors = [{ name: label ?? "", roll: dice }];
if (extraDice) {
rollDescriptors.push({ name: "", roll: extraDice });
}
TS.dice.putDiceInTray(rollDescriptors);
} else {
window.open(uri, "_self");
}
};
triggerTalespire(name, dice, extraDice);
});

if (diceLabel) {
link.innerText = diceLabel;
Expand Down

0 comments on commit 10abcf7

Please sign in to comment.