diff --git a/.eslintrc.js b/.eslintrc.js
index 387043b..6597ca8 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -22,5 +22,6 @@ module.exports = {
rules: {
quotes: ["error", "double"],
"no-shadow": ["error", { hoist: "functions", allow: [] }],
+ "no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
},
};
diff --git a/css/panel.css b/css/panel.css
index 01b24d3..a0b274e 100644
--- a/css/panel.css
+++ b/css/panel.css
@@ -37,21 +37,25 @@
justify-content: space-between;
}
-.panel-row button {
+.panel-row ui-button,
+.panel-row color-button,
+.panel-row tool-button,
+.panel-row variant-button,
+.panel-row variant-stamp-button {
margin-right: var(--button-space);
- width: var(--button-size);
- height: var(--button-size);
- border: 2px solid black;
- border-radius: 6px;
}
-.panel-row button:last-child {
+.panel-row ui-button:last-child,
+.panel-row color-button:last-child,
+.panel-row tool-button:last-child,
+.panel-row variant-button:last-child,
+.panel-row variant-stamp-button:last-child {
margin-right: 0px;
}
-.panel-row button.active {
- border: 2px solid white;
- box-shadow: 0px 0px 0px 4px orange;
+#tools {
+ display: flex;
+ flex: 1;
}
#actions {
diff --git a/index.html b/index.html
index 6e101c9..9756504 100644
--- a/index.html
+++ b/index.html
@@ -76,9 +76,9 @@
New version available
diff --git a/js/boot.mjs b/js/boot.mjs
index ef6951a..8bfc0ed 100644
--- a/js/boot.mjs
+++ b/js/boot.mjs
@@ -22,6 +22,19 @@ import { attachKeyboardListeners } from "./controls/keyboard.mjs";
import { attachGamepadBlockListeners } from "./controls/general.mjs";
import { attachGamepadListeners } from "./controls/gamepad.mjs";
import { initializeCursor } from "./cursor.mjs";
+import { ColorButton } from "./ui/color.mjs";
+import { ToolButton } from "./ui/tool.mjs";
+import { VariantButton } from "./ui/variant.mjs";
+import { VariantStampButton } from "./ui/variant-stamp.mjs";
+import { UiButton } from "./ui/button.mjs";
+
+function registerComponents() {
+ customElements.define("ui-button", UiButton);
+ customElements.define("color-button", ColorButton);
+ customElements.define("tool-button", ToolButton);
+ customElements.define("variant-button", VariantButton);
+ customElements.define("variant-stamp-button", VariantStampButton);
+}
function attachResizeListeners() {
const canvas = getCanvas();
@@ -59,6 +72,7 @@ export function boot() {
const state = createState();
const canvas = getCanvas();
+ registerComponents();
restorePreviousCanvas(canvas);
attachCanvasSaveListener(canvas);
diff --git a/js/controls/keyboard.mjs b/js/controls/keyboard.mjs
index d36be2d..5776326 100644
--- a/js/controls/keyboard.mjs
+++ b/js/controls/keyboard.mjs
@@ -39,15 +39,7 @@ export function attachKeyboardListeners(state) {
let accelerationTimer = null;
let keysPressed = NO_KEYS_PRESSED;
- function shouldBlockInteractions() {
- return state.get((prevState) => prevState.blockedInteractions);
- }
-
window.addEventListener("keyup", (event) => {
- if (shouldBlockInteractions()) {
- return;
- }
-
switch (event.key) {
case "ArrowUp":
keysPressed = produceMovementKeysPressed(keysPressed, "up", false);
@@ -67,10 +59,6 @@ export function attachKeyboardListeners(state) {
});
window.addEventListener("keydown", (event) => {
- if (shouldBlockInteractions()) {
- return;
- }
-
if (event.key === "p") {
setTool(TOOLS.PEN, { state });
} else if (event.key === "f") {
diff --git a/js/cursor.mjs b/js/cursor.mjs
index 7e43c71..d379970 100644
--- a/js/cursor.mjs
+++ b/js/cursor.mjs
@@ -23,19 +23,11 @@ export function initializeCursor({ state }) {
const y = cursor.y;
let setCursorTimer = null;
- function shouldBlockInteractions() {
- return state.get((prevState) => prevState.blockedInteractions);
- }
-
function drawCursorOnMouseMove(event) {
if (setCursorTimer) {
window.clearTimeout(setCursorTimer);
}
- if (shouldBlockInteractions()) {
- return;
- }
-
const rect = canvas.getBoundingClientRect();
const { clientX, clientY } = event;
diff --git a/js/dom.mjs b/js/dom.mjs
index 7adb358..e772319 100644
--- a/js/dom.mjs
+++ b/js/dom.mjs
@@ -12,10 +12,28 @@ export function getPanelTools() {
return document.getElementById("tools");
}
+export function getToolButtons() {
+ return getPanelTools().querySelectorAll("tool-button");
+}
+
+export function getToolButtonById(id) {
+ return Array.from(getToolButtons()).find((button) => button.id === id);
+}
+
export function getPanelToolVariants() {
return document.getElementById("variants");
}
+export function getVariantButtons() {
+ return getPanelToolVariants().querySelectorAll(
+ "variant-button,variant-stamp-button",
+ );
+}
+
+export function getVariantButtonById(id) {
+ return Array.from(getVariantButtons()).find((button) => button.id === id);
+}
+
export function getPanelToolActions() {
return document.getElementById("actions");
}
@@ -24,6 +42,14 @@ export function getPanelColors() {
return document.getElementById("colors");
}
+export function getColorButtons() {
+ return getPanelColors().querySelectorAll("color-button");
+}
+
+export function getColorButtonByColor(color) {
+ return Array.from(getColorButtons()).find((button) => button.color === color);
+}
+
export function getPanel() {
return document.getElementById("panel");
}
diff --git a/js/state/actions/cam.mjs b/js/state/actions/cam.mjs
index 20e7321..ad36044 100644
--- a/js/state/actions/cam.mjs
+++ b/js/state/actions/cam.mjs
@@ -6,7 +6,6 @@ import {
getCountdownAnimationLengthInSeconds,
getFlashAnimationLengthInSeconds,
} from "../../dom.mjs";
-import { blockInteractions, unblockInteractions } from "../actions/ui.mjs";
function memorizePhoto({ state }) {
state.set(() => ({
@@ -29,7 +28,6 @@ export function takePhoto({ state }) {
const flashAnimationLength = getFlashAnimationLengthInSeconds() * 1000;
insertCountdown();
- blockInteractions({ state });
const videoSettings = cam.srcObject.getVideoTracks()[0]?.getSettings();
const height = canvas.height;
@@ -39,7 +37,6 @@ export function takePhoto({ state }) {
ctx.drawImage(cam, canvas.width / 2 - width / 2, 0, width, height);
memorizePhoto({ state });
removeCountdown();
- unblockInteractions({ state });
}, countdownAnimationLength + flashAnimationLength);
}
diff --git a/js/state/actions/ui.mjs b/js/state/actions/ui.mjs
index 3c39d1a..3fb3d54 100644
--- a/js/state/actions/ui.mjs
+++ b/js/state/actions/ui.mjs
@@ -1,17 +1,5 @@
import { getInfoDialog } from "../../dom.mjs";
-export function blockInteractions({ state }) {
- state.set(() => ({
- blockedInteractions: true,
- }));
-}
-
-export function unblockInteractions({ state }) {
- state.set(() => ({
- blockedInteractions: false,
- }));
-}
-
export function showInfo() {
const dialog = getInfoDialog();
const closeButton = dialog.querySelector("#close-info");
diff --git a/js/state/state.mjs b/js/state/state.mjs
index f23e31e..5b877e3 100644
--- a/js/state/state.mjs
+++ b/js/state/state.mjs
@@ -17,7 +17,6 @@ export function createState() {
color: loadColor(),
gamepad: null,
photoMemorized: false,
- blockedInteractions: false,
gamepadBlocked: false,
};
diff --git a/js/tools/fill.mjs b/js/tools/fill.mjs
index 72f1411..6a81746 100644
--- a/js/tools/fill.mjs
+++ b/js/tools/fill.mjs
@@ -4,10 +4,6 @@ import {
isPrimaryGamepadButtonPressed,
} from "../controls/gamepad.mjs";
import { isWithinCanvasBounds } from "../canvas.mjs";
-import {
- blockInteractions,
- unblockInteractions,
-} from "../state/actions/ui.mjs";
function hexToRGB(h) {
let r = 0,
@@ -53,9 +49,8 @@ function colorsMatch(a, b) {
}
// taken from SO: https://stackoverflow.com/a/56221940/3056783
-function floodFill(ctx, x, y, fillColor, { state }) {
+function floodFill(ctx, x, y, fillColor) {
showLoader();
- blockInteractions({ state });
// read the pixels in the canvas
const imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
@@ -83,7 +78,6 @@ function floodFill(ctx, x, y, fillColor, { state }) {
ctx.putImageData(imageData, 0, 0);
}
- unblockInteractions({ state });
hideLoader();
}
@@ -131,18 +125,6 @@ export function activateFill({ state }) {
window.removeEventListener("keydown", keyDown);
}
- function onBlockInteractionsChange(nextState, prevState) {
- if (nextState.blockedInteractions === prevState.blockedInteractions) {
- return;
- }
-
- if (nextState.blockedInteractions) {
- deactivateListeners();
- } else {
- activateListeners();
- }
- }
-
let wasPressed = false;
function activateFillOnGamepadButtonPress() {
const gamepad = getGamepad(state);
@@ -187,20 +169,9 @@ export function activateFill({ state }) {
frame = null;
}
- const blockedInteractions = state.get(
- (prevState) => prevState.blockedInteractions,
- );
-
- if (blockedInteractions) {
- deactivateListeners();
- } else {
- activateListeners();
- }
-
- state.addListener(onBlockInteractionsChange);
+ activateListeners();
return function dispose() {
deactivateListeners();
- state.removeListener(onBlockInteractionsChange);
};
}
diff --git a/js/tools/pen.mjs b/js/tools/pen.mjs
index 2d50405..8431f34 100644
--- a/js/tools/pen.mjs
+++ b/js/tools/pen.mjs
@@ -201,27 +201,7 @@ export function activatePen({ state, variant }) {
cancelGamepadAnimationFrame();
}
- function onBlockInteractionsChange(nextState, prevState) {
- if (nextState.blockedInteractions === prevState.blockedInteractions) {
- return;
- }
-
- if (nextState.blockedInteractions) {
- deactivateListeners();
- } else {
- activateListeners();
- }
- }
-
- const blockInteractions = state.get(
- (prevState) => prevState.blockedInteractions,
- );
-
- if (blockInteractions) {
- deactivateListeners();
- } else {
- activateListeners();
- }
+ activateListeners();
function updateColor(nextState, prevState) {
if (nextState.color === prevState.color) {
@@ -243,13 +223,11 @@ export function activatePen({ state, variant }) {
draw(nextState.cursor.x, nextState.cursor.y);
}
- state.addListener(onBlockInteractionsChange);
state.addListener(updateColor);
state.addListener(updatePath);
return function dispose() {
deactivateListeners();
- state.removeListener(onBlockInteractionsChange);
state.removeListener(updateColor);
state.removeListener(updatePath);
};
diff --git a/js/ui/actions.mjs b/js/ui/actions.mjs
index 7d06672..8e80e47 100644
--- a/js/ui/actions.mjs
+++ b/js/ui/actions.mjs
@@ -1,15 +1,11 @@
import { takePhoto, removePhoto } from "../state/actions/cam.mjs";
import { getPanelToolActions } from "../dom.mjs";
-import { disposeCallback, ensureCallbacksRemoved, loadIcon } from "./utils.mjs";
+import { UiButton } from "./button.mjs";
-export function buildToolActions(tool, state) {
+export function buildToolActions(tool, { state, signal }) {
const actionsContainer = getPanelToolActions();
- const listeners = {};
-
tool.actions.forEach((action) => {
- const button = document.createElement("button");
-
function onCamTakePhotoClick() {
takePhoto({ state });
}
@@ -18,45 +14,48 @@ export function buildToolActions(tool, state) {
removePhoto({ state });
}
+ let button = null;
+
switch (action.id.description) {
case "cam-take-photo": {
- listeners[action.id.description] = onCamTakePhotoClick;
- button.addEventListener("click", onCamTakePhotoClick);
+ button = new UiButton({
+ ariaLabel: action.id.description,
+ dataset: {
+ value: action.id.description,
+ },
+ iconUrl: action.iconUrl,
+ onClick: onCamTakePhotoClick,
+ signal,
+ });
+
break;
}
case "cam-cancel": {
- listeners[action.id.description] = onCamCancelClick;
- button.addEventListener("click", onCamCancelClick);
+ button = new UiButton({
+ ariaLabel: action.id.description,
+ dataset: {
+ value: action.id.description,
+ },
+ iconUrl: action.iconUrl,
+ onClick: onCamCancelClick,
+ signal,
+ });
break;
}
}
- button.dataset.value = action.id.description;
- button.innerText = action.id.description;
-
- loadIcon(action.iconUrl)
- .then((icon) => {
- button.innerHTML = icon;
- })
- .catch((error) => {
- console.error(error);
- button.innerText = action.id.description;
- });
-
actionsContainer.appendChild(button);
});
- return function dispose() {
+ function dispose() {
Array.from(actionsContainer.querySelectorAll("button")).forEach(
(button) => {
- disposeCallback(button, listeners);
-
button.remove();
},
);
- ensureCallbacksRemoved(listeners);
-
actionsContainer.innerHTML = "";
- };
+ }
+
+ signal.addEventListener("abort", dispose, { once: true });
}
diff --git a/js/ui/button.mjs b/js/ui/button.mjs
new file mode 100644
index 0000000..fd88cf8
--- /dev/null
+++ b/js/ui/button.mjs
@@ -0,0 +1,151 @@
+import { loadIcon } from "./utils.mjs";
+import { serializeSvg, deserializeSvgFromDataURI } from "../svg-utils.mjs";
+import { isDataUri } from "../state/utils.mjs";
+
+/**
+ * This is a base UI button with square shape.
+ */
+export class UiButton extends HTMLElement {
+ // Can be built also declaratively with HTML.
+ constructor(options = {}) {
+ const {
+ isActive,
+ id,
+ ariaLabel,
+ dataset,
+ iconUrl,
+ backgroundColor,
+ onClick,
+ signal,
+ } = options;
+
+ super();
+
+ this.#isActive = isActive ?? false;
+ this.#id = id;
+ this.#onClick = onClick;
+ this.#signal = signal;
+ this.#ariaLabel = ariaLabel;
+ this.#dataset = dataset;
+ this.#iconUrl = iconUrl;
+ this.#backgroundColor = backgroundColor;
+
+ this.attachShadow({ mode: "open" });
+ }
+
+ #isActive = false;
+ #id = "";
+ #iconUrl = null;
+ #ariaLabel = "";
+ #dataset = {};
+ #backgroundColor = "#000000";
+ #onClick = () => {};
+ #signal = null;
+
+ connectedCallback() {
+ this.shadowRoot.innerHTML = `
+
+
+ `;
+
+ if (this.#onClick) {
+ this.addClickListener(this.#onClick);
+ }
+ this.iconUrl = this.#iconUrl || this.getAttribute("icon-url");
+ this.isActive = false;
+ }
+
+ attributeChangedCallback(name, oldValue, newValue) {
+ switch (name) {
+ case "icon-url":
+ if (oldValue === newValue) {
+ break;
+ }
+ this.iconUrl = newValue;
+ break;
+ }
+ }
+
+ click(e) {
+ this.button.dispatchEvent(e);
+ }
+
+ addClickListener(listener) {
+ this.button.addEventListener("click", listener, {
+ signal: this.#signal,
+ });
+ }
+
+ removeClickListener(listener) {
+ this.button.removeEventListener("click", listener);
+ }
+
+ get button() {
+ return this.shadowRoot.querySelector("button");
+ }
+
+ set isActive(value) {
+ this.#isActive = value;
+ this.button.setAttribute("aria-pressed", value ? "true" : "false");
+ }
+
+ get isActive() {
+ return this.#isActive;
+ }
+
+ set iconUrl(value) {
+ if (!value) {
+ return;
+ }
+
+ const dataset = this.#dataset ?? this.dataset;
+
+ if (isDataUri(value)) {
+ this.button.innerHTML = serializeSvg(deserializeSvgFromDataURI(value));
+ } else {
+ loadIcon(value)
+ .then((icon) => {
+ this.button.innerHTML = icon;
+ })
+ .catch((error) => {
+ console.error(error);
+ this.button.innerText = UiButton.#getContentFallback(dataset);
+ });
+ }
+ }
+
+ static observedAttributes = ["icon-url"];
+
+ static #createDataSetAttributesString(dataset) {
+ return Object.entries(dataset ?? {})
+ .map(([key, value]) => `data-${key}="${value}"`)
+ .join(" ");
+ }
+
+ static #getContentFallback(dataset) {
+ return dataset?.id?.description ?? "";
+ }
+}
diff --git a/js/ui/color.mjs b/js/ui/color.mjs
new file mode 100644
index 0000000..9483f1f
--- /dev/null
+++ b/js/ui/color.mjs
@@ -0,0 +1,54 @@
+import { COLOR } from "../state/constants.mjs";
+import { UiButton } from "./button.mjs";
+
+/**
+ * Represents a button for selecting colors
+ */
+export class ColorButton extends UiButton {
+ constructor({ onClick, color, signal, isActive }) {
+ super();
+
+ this.color = color;
+ this.#isActive = isActive;
+ this.#onClick = onClick;
+ this.#signal = signal;
+ }
+
+ color = "#000000";
+ #onClick = () => {};
+ #signal = null;
+ #isActive = false;
+
+ connectedCallback() {
+ const button = new UiButton({
+ ariaLabel: `${Object.entries(COLOR)
+ .find(([_, value]) => value === this.color)?.[0]
+ ?.toLocaleLowerCase()} color`,
+ dataset: {
+ value: this.color,
+ },
+ backgroundColor: this.color,
+ onClick: this.#onClick,
+ signal: this.#signal,
+ });
+
+ this.shadowRoot.appendChild(button);
+ this.isActive = this.#isActive;
+ }
+
+ click(e) {
+ this.#button.click(e);
+ }
+
+ set isActive(value) {
+ this.shadowRoot.querySelector("ui-button").isActive = value;
+ }
+
+ get isActive() {
+ return this.shadowRoot.querySelector("ui-button").isActive;
+ }
+
+ get #button() {
+ return this.shadowRoot.querySelector("ui-button").button;
+ }
+}
diff --git a/js/ui/colors.mjs b/js/ui/colors.mjs
index 1eac578..30d61cf 100644
--- a/js/ui/colors.mjs
+++ b/js/ui/colors.mjs
@@ -9,12 +9,16 @@ import {
isLeftShoulderGamepadButtonPressed,
getGamepad,
} from "../controls/gamepad.mjs";
-import { getPanelColors } from "../dom.mjs";
-import { updateActivatedButton } from "./utils.mjs";
+import {
+ getPanelColors,
+ getColorButtons,
+ getColorButtonByColor,
+} from "../dom.mjs";
+import { ColorButton } from "./color.mjs";
const GAMEPAD_BUTTON_ACTIVATION_DELAY_IN_MS = 300;
-function attachKeyboardListeners(state) {
+function attachKeyboardListeners({ state, signal }) {
function onKeyDown(event) {
switch (event.key) {
case "a":
@@ -28,11 +32,7 @@ function attachKeyboardListeners(state) {
}
}
- window.addEventListener("keydown", onKeyDown);
-
- return function dispose() {
- window.removeEventListener("keydown", onKeyDown);
- };
+ window.addEventListener("keydown", onKeyDown, { signal });
}
function attachGamepadListeners(state) {
@@ -75,72 +75,43 @@ function attachGamepadListeners(state) {
frame = requestAnimationFrame(activateColorCycleOnShoulderButtonPresses);
}
- function cancelGamepadAnimationFrame() {
- cancelAnimationFrame(frame);
- frame = null;
- }
-
requestGamepadAnimationFrame();
-
- return function dispose() {
- cancelGamepadAnimationFrame();
- };
}
export function createColorPanel({ state }) {
const colors = getPanelColors();
- let disposeKeyboardListenersCallback = null;
- let disposeGamepadListenersCallback = null;
+ let controller = new AbortController();
state.addListener((nextState, prevState) => {
if (nextState.color === prevState.color) {
return;
}
- updateActivatedButton(colors, nextState.color);
- });
+ const prevActiveButton = Array.from(getColorButtons()).find(
+ (b) => b.isActive,
+ );
+ prevActiveButton.isActive = false;
- state.addListener((nextState, prevState) => {
- if (nextState.blockedInteractions === prevState.blockedInteractions) {
- return;
- }
+ const nextActiveButton = getColorButtonByColor(nextState.color);
- if (nextState.blockedInteractions) {
- if (disposeKeyboardListenersCallback) {
- disposeKeyboardListenersCallback();
- disposeKeyboardListenersCallback = null;
- }
-
- if (disposeGamepadListenersCallback) {
- disposeGamepadListenersCallback();
- disposeGamepadListenersCallback = null;
- }
- } else {
- disposeKeyboardListenersCallback = attachKeyboardListeners(state);
- disposeGamepadListenersCallback = attachGamepadListeners(state);
+ if (!nextActiveButton) {
+ throw new Error(`Color button not found for color: ${nextState.color}`);
}
+
+ nextActiveButton.isActive = true;
});
COLOR_LIST.forEach((color) => {
- const button = document.createElement("button");
- button.style.backgroundColor = color;
-
- button.addEventListener("click", () => {
- setColor(color, { state });
+ const colorButton = new ColorButton({
+ color,
+ onClick: () => setColor(color, { state }),
+ signal: controller.signal,
+ isActive: state.get((prevState) => prevState.color) === color,
});
- button.dataset.value = color;
- button.style.backgroundColor = color;
-
- colors.appendChild(button);
+ colors.appendChild(colorButton);
});
- const selectedColor = state.get((prevState) => prevState.color);
-
- if (selectedColor) {
- updateActivatedButton(colors, selectedColor);
- }
-
- disposeKeyboardListenersCallback = attachKeyboardListeners(state);
- disposeGamepadListenersCallback = attachGamepadListeners(state);
+ attachKeyboardListeners({ state, signal: controller.signal });
+ attachGamepadListeners(state);
}
diff --git a/js/ui/global.mjs b/js/ui/global.mjs
index fe51935..58207bb 100644
--- a/js/ui/global.mjs
+++ b/js/ui/global.mjs
@@ -3,6 +3,7 @@ import { resetCanvas, exportImage } from "../state/actions/canvas.mjs";
import { showInfo } from "../state/actions/ui.mjs";
export function createGlobalActionsPanel() {
+ const controller = new AbortController();
const clearButton = getClearButton();
const saveButton = getSaveButton();
const infoButton = getInfoButton();
@@ -27,13 +28,20 @@ export function createGlobalActionsPanel() {
showInfo();
}
- clearButton.addEventListener("click", handleClearClick);
- saveButton.addEventListener("click", handleSaveClick);
- infoButton.addEventListener("click", handleInfoClick);
+ clearButton.addClickListener(handleClearClick, {
+ once: true,
+ signal: controller.signal,
+ });
+ saveButton.addClickListener(handleSaveClick, {
+ once: true,
+ signal: controller.signal,
+ });
+ infoButton.addClickListener(handleInfoClick, {
+ once: true,
+ signal: controller.signal,
+ });
return function dispose() {
- clearButton.removeEventListener("click", handleClearClick);
- saveButton.removeEventListener("click", handleSaveClick);
- infoButton.removeEventListener("click", handleInfoClick);
+ // TODO: in case these need to be disposed call `controller.abort()`
};
}
diff --git a/js/ui/panel.mjs b/js/ui/panel.mjs
index 316c0e6..2364c69 100644
--- a/js/ui/panel.mjs
+++ b/js/ui/panel.mjs
@@ -4,9 +4,12 @@ import {
} from "../controls/gamepad.mjs";
import { getPanel } from "../dom.mjs";
import { isCursorWithinPanelBounds } from "./utils.mjs";
+import { UiButton } from "./button.mjs";
function getPanelButtonByCoordinates(x, y, panel) {
- const buttons = panel.querySelectorAll("button");
+ const buttons = panel.querySelectorAll(
+ "ui-button,color-button,tool-button,variant-button,variant-stamp-button",
+ );
for (let i = 0; i < buttons.length; i++) {
const button = buttons[i];
@@ -43,6 +46,12 @@ function activatePanelButtonOnCoordinates(x, y) {
bubbles: false,
});
+ if (button instanceof UiButton) {
+ button.click(clickEvent);
+
+ return;
+ }
+
button.dispatchEvent(clickEvent);
}
@@ -152,36 +161,5 @@ export function attachPanelListeners({ state }) {
disposeGamepadListenersCallback = attachGamepadListeners(state);
}
- function deactivateListeners() {
- if (disposeMouseListenersCallback) {
- disposeMouseListenersCallback();
- disposeMouseListenersCallback = null;
- }
-
- if (disposeKeyboardListenersCallback) {
- disposeKeyboardListenersCallback();
- disposeKeyboardListenersCallback = null;
- }
-
- if (disposeGamepadListenersCallback) {
- disposeGamepadListenersCallback();
- disposeGamepadListenersCallback = null;
- }
- }
-
- function onBlockedInteractionsChange(nextState, prevState) {
- if (nextState.blockedInteractions === prevState.blockedInteractions) {
- return;
- }
-
- if (nextState.blockedInteractions) {
- deactivateListeners();
- } else {
- activateListeners();
- }
- }
-
- state.addListener(onBlockedInteractionsChange);
-
activateListeners();
}
diff --git a/js/ui/tool.mjs b/js/ui/tool.mjs
new file mode 100644
index 0000000..3266f7c
--- /dev/null
+++ b/js/ui/tool.mjs
@@ -0,0 +1,56 @@
+import { UiButton } from "./button.mjs";
+import { setTool } from "../state/actions/tool.mjs";
+
+/**
+ * Represents tool/action button.
+ */
+export class ToolButton extends UiButton {
+ constructor({ tool, state, isActive }) {
+ super();
+
+ this.id = tool.id;
+ this.#isActive = isActive;
+ this.#tool = tool;
+ this.#state = state;
+ }
+
+ #isActive = false;
+ id = "";
+ #state = null;
+ #tool = null;
+
+ connectedCallback() {
+ const button = new UiButton({
+ ariaLabel: this.#tool.id.description,
+ dataset: {
+ id: this.id.description,
+ value: this.#tool.id.description,
+ },
+ iconUrl: this.#tool.iconUrl,
+ onClick: this.#onClick,
+ });
+
+ this.shadowRoot.appendChild(button);
+ button.isActive = this.#isActive;
+ }
+
+ click(e) {
+ this.#button.click(e);
+ }
+
+ set isActive(value) {
+ this.shadowRoot.querySelector("ui-button").isActive = value;
+ }
+
+ get isActive() {
+ return this.shadowRoot.querySelector("ui-button").isActive;
+ }
+
+ get #button() {
+ return this.shadowRoot.querySelector("ui-button").button;
+ }
+
+ #onClick = () => {
+ setTool(this.#tool, { state: this.#state });
+ };
+}
diff --git a/js/ui/tools.mjs b/js/ui/tools.mjs
index 48903a1..2cb175d 100644
--- a/js/ui/tools.mjs
+++ b/js/ui/tools.mjs
@@ -1,77 +1,84 @@
-import { getPanelTools } from "../dom.mjs";
-import { setTool } from "../state/actions/tool.mjs";
+import { getPanelTools, getToolButtons, getToolButtonById } from "../dom.mjs";
import { TOOL_LIST } from "../state/constants.mjs";
-import { loadIcon, updateActivatedButton } from "./utils.mjs";
import { buildToolActions } from "./actions.mjs";
import { buildToolVariants } from "./variants.mjs";
+import { ToolButton } from "./tool.mjs";
export function createToolPanel({ state }) {
const tools = getPanelTools();
- let disposeVariantsCallback = null;
- let disposeActionsCallback = null;
+ let actionsController = new AbortController();
+ let variantsController = new AbortController();
state.addListener((updatedState, prevState) => {
if (updatedState.tool === prevState.tool) {
return;
}
- if (disposeVariantsCallback) {
- disposeVariantsCallback();
- disposeVariantsCallback = null;
+ if (!variantsController.signal.aborted) {
+ variantsController.abort();
}
- if (disposeActionsCallback) {
- disposeActionsCallback();
- disposeActionsCallback = null;
+ if (!actionsController.signal.aborted) {
+ actionsController.abort();
}
- updateActivatedButton(tools, updatedState.tool.id.description);
+ variantsController = new AbortController();
+ actionsController = new AbortController();
+
+ const prevActiveButton = Array.from(getToolButtons()).find(
+ (b) => b.isActive,
+ );
+ prevActiveButton.isActive = false;
+
+ const toolId = updatedState.tool.id;
+ const nextActiveButton = getToolButtonById(toolId);
+
+ if (!nextActiveButton) {
+ throw new Error(`Tool button not found for tool: ${toolId}`);
+ }
+
+ nextActiveButton.isActive = true;
if (updatedState.tool.variants) {
- disposeVariantsCallback = buildToolVariants(updatedState.tool, state);
+ buildToolVariants(updatedState.tool, {
+ state,
+ signal: variantsController.signal,
+ });
}
if (updatedState.tool.actions) {
- disposeActionsCallback = buildToolActions(updatedState.tool, state);
+ buildToolActions(updatedState.tool, {
+ state,
+ signal: actionsController.signal,
+ });
}
});
- TOOL_LIST.forEach((tool) => {
- const button = document.createElement("button");
-
- button.addEventListener(
- "click",
- () => {
- setTool(tool, { state });
- },
- true,
- );
-
- button.dataset.value = tool.id.description;
+ const selectedTool = state.get((prevState) => prevState.tool);
- loadIcon(tool.iconUrl)
- .then((icon) => {
- button.innerHTML = icon;
- })
- .catch((error) => {
- console.error(error);
- button.innerText = tool.id.description;
- });
+ TOOL_LIST.forEach((tool) => {
+ const button = new ToolButton({
+ tool,
+ state,
+ isActive: selectedTool.id === tool.id,
+ });
tools.appendChild(button);
});
- const selectedTool = state.get((prevState) => prevState.tool);
-
if (selectedTool) {
- updateActivatedButton(tools, selectedTool.id.description);
-
if (selectedTool.variants) {
- disposeVariantsCallback = buildToolVariants(selectedTool, state);
+ buildToolVariants(selectedTool, {
+ state,
+ signal: variantsController.signal,
+ });
}
if (selectedTool.actions) {
- disposeActionsCallback = buildToolActions(selectedTool, state);
+ buildToolActions(selectedTool, {
+ state,
+ signal: actionsController.signal,
+ });
}
}
}
diff --git a/js/ui/utils.mjs b/js/ui/utils.mjs
index 89b5704..5ab69ad 100644
--- a/js/ui/utils.mjs
+++ b/js/ui/utils.mjs
@@ -27,18 +27,6 @@ export function ensureCallbacksRemoved(listeners) {
throw new Error("Not all listeners were removed!");
}
-export function updateActivatedButton(buttonContainer, value) {
- const buttons = buttonContainer.querySelectorAll("button");
-
- Array.from(buttons).forEach((button) => {
- if (button.dataset.value === value) {
- button.classList.add("active");
- } else {
- button.classList.remove("active");
- }
- });
-}
-
/**
* Is cursor located on UI panel?
*
diff --git a/js/ui/variant-stamp.mjs b/js/ui/variant-stamp.mjs
new file mode 100644
index 0000000..2179d64
--- /dev/null
+++ b/js/ui/variant-stamp.mjs
@@ -0,0 +1,100 @@
+import { setCustomVariant, setTool } from "../state/actions/tool.mjs";
+import { VariantButton } from "./variant.mjs";
+
+import {
+ createSvgDataUri,
+ serializeSvg,
+ deserializeSvgFromDataURI,
+ normalizeSvgSize,
+} from "../svg-utils.mjs";
+
+export class VariantStampButton extends VariantButton {
+ constructor(options) {
+ super(options);
+
+ const { variant, tool, state } = options;
+ this.#variant = variant;
+ this.#tool = tool;
+ this.#state = state;
+ }
+
+ #variant = null;
+ #tool = null;
+ #state = null;
+
+ connectedCallback() {
+ super.connectedCallback();
+ }
+
+ click = () => {
+ if (this.#variant.value) {
+ setTool(this.#tool, { state: this.#state, variant: this.#variant });
+ return;
+ }
+
+ const fileInput = document.createElement("input");
+ fileInput.type = "file";
+ fileInput.accept = "image/svg+xml";
+ fileInput.style.display = "none";
+
+ const handleFileUpload = (event) => {
+ this.#readUploadedSVG(event, fileInput);
+ };
+
+ fileInput.addEventListener("stamp-custom-slot-success", (event) => {
+ fileInput.removeEventListener("change", handleFileUpload);
+ fileInput.remove();
+
+ const updatedVariant = {
+ ...this.#variant,
+ iconUrl: event.detail.iconDataUri,
+ value: event.detail.dataUri,
+ };
+ setTool(this.#tool, { state: this.#state, variant: updatedVariant });
+ setCustomVariant(this.#tool, updatedVariant, { state: this.#state });
+ this.#variant = updatedVariant;
+ super.uiButton.setAttribute("icon-url", event.detail.iconDataUri);
+ });
+ fileInput.addEventListener("stamp-custom-slot-failure", () => {
+ alert("Something went wrong with uploading the image!");
+ });
+ fileInput.addEventListener("change", handleFileUpload);
+
+ fileInput.click();
+ };
+
+ #readUploadedSVG = (event, fileInput) => {
+ const file = event.target.files[0];
+ const failureEvent = new CustomEvent("stamp-custom-slot-failure");
+
+ if (!file) {
+ fileInput.dispatchEvent(failureEvent);
+ throw new Error("No file selected!");
+ }
+
+ const fileReader = new FileReader();
+ fileReader.addEventListener("load", (fileEvent) => {
+ const parsedSvgElement = deserializeSvgFromDataURI(
+ fileEvent.srcElement.result,
+ );
+ const iconSvgDocument = parsedSvgElement.documentElement.cloneNode(true);
+ const stampSvgDocument = parsedSvgElement.documentElement.cloneNode(true);
+ const iconSvgElement = normalizeSvgSize(iconSvgDocument);
+ const stampSvgElement = normalizeSvgSize(stampSvgDocument, 50);
+
+ fileInput.dispatchEvent(
+ new CustomEvent("stamp-custom-slot-success", {
+ detail: {
+ iconDataUri: createSvgDataUri(serializeSvg(iconSvgElement)),
+ dataUri: createSvgDataUri(serializeSvg(stampSvgElement)),
+ },
+ }),
+ );
+ });
+ fileReader.addEventListener("error", () => {
+ fileInput.dispatchEvent(failureEvent);
+ });
+
+ fileReader.readAsDataURL(file);
+ };
+}
diff --git a/js/ui/variant.mjs b/js/ui/variant.mjs
new file mode 100644
index 0000000..734ab8b
--- /dev/null
+++ b/js/ui/variant.mjs
@@ -0,0 +1,59 @@
+import { UiButton } from "./button.mjs";
+import { setTool } from "../state/actions/tool.mjs";
+
+export class VariantButton extends UiButton {
+ constructor({ id, iconUrl, signal, isActive, tool, state, variant }) {
+ super();
+
+ this.#isActive = isActive;
+ this.id = id;
+ this.#tool = tool;
+ this.#state = state;
+ this.#variant = variant;
+ this.#iconUrl = iconUrl;
+ this.#signal = signal;
+ }
+
+ #isActive = false;
+ id = "";
+ #iconUrl = "";
+ #signal = null;
+ #tool = null;
+ #state = null;
+ #variant = null;
+
+ connectedCallback() {
+ const button = new UiButton({
+ ariaLabel: `${this.id.description.toLocaleLowerCase()} variant}`,
+ dataset: {
+ value: this.id.description,
+ },
+ iconUrl: this.#iconUrl,
+ onClick: this.click,
+ signal: this.#signal,
+ });
+
+ this.shadowRoot.appendChild(button);
+ this.isActive = this.#isActive;
+ }
+
+ click = () => {
+ setTool(this.#tool, { state: this.#state, variant: this.#variant });
+ };
+
+ get button() {
+ return this.shadowRoot.querySelector("ui-button").button;
+ }
+
+ get uiButton() {
+ return this.shadowRoot.querySelector("ui-button");
+ }
+
+ set isActive(value) {
+ this.shadowRoot.querySelector("ui-button").isActive = value;
+ }
+
+ get isActive() {
+ return this.shadowRoot.querySelector("ui-button").isActive;
+ }
+}
diff --git a/js/ui/variants.mjs b/js/ui/variants.mjs
index 1889418..62f782c 100644
--- a/js/ui/variants.mjs
+++ b/js/ui/variants.mjs
@@ -1,111 +1,19 @@
-import { setTool, setCustomVariant } from "../state/actions/tool.mjs";
import { TOOLS } from "../state/constants.mjs";
-import { isDataUri } from "../state/utils.mjs";
-import { getPanelToolVariants } from "../dom.mjs";
+import {
+ getPanelToolVariants,
+ getVariantButtons,
+ getVariantButtonById,
+} from "../dom.mjs";
import {
isLeftTriggerGamepadButtonPressed,
isRightTriggerGamepadButtonPressed,
getGamepad,
} from "../controls/gamepad.mjs";
-import {
- loadIcon,
- disposeCallback,
- ensureCallbacksRemoved,
- updateActivatedButton,
-} from "./utils.mjs";
-import {
- createSvgDataUri,
- serializeSvg,
- deserializeSvgFromDataURI,
- normalizeSvgSize,
-} from "../svg-utils.mjs";
+import { VariantButton } from "./variant.mjs";
+import { VariantStampButton } from "./variant-stamp.mjs";
const GAMEPAD_BUTTON_ACTIVATION_DELAY_IN_MS = 300;
-function readUploadedSVG(event, fileInput) {
- const file = event.target.files[0];
- const failureEvent = new CustomEvent("stamp-custom-slot-failure");
-
- if (!file) {
- fileInput.dispatchEvent(failureEvent);
- throw new Error("No file selected!");
- }
-
- const fileReader = new FileReader();
- fileReader.addEventListener("load", (fileEvent) => {
- const parsedSvgElement = deserializeSvgFromDataURI(
- fileEvent.srcElement.result,
- );
- const iconSvgDocument = parsedSvgElement.documentElement.cloneNode(true);
- const stampSvgDocument = parsedSvgElement.documentElement.cloneNode(true);
- const iconSvgElement = normalizeSvgSize(iconSvgDocument);
- const stampSvgElement = normalizeSvgSize(stampSvgDocument, 50);
-
- fileInput.dispatchEvent(
- new CustomEvent("stamp-custom-slot-success", {
- detail: {
- iconDataUri: createSvgDataUri(serializeSvg(iconSvgElement)),
- dataUri: createSvgDataUri(serializeSvg(stampSvgElement)),
- },
- }),
- );
- });
- fileReader.addEventListener("error", () => {
- fileInput.dispatchEvent(failureEvent);
- });
-
- fileReader.readAsDataURL(file);
-}
-
-function createFileInputForUpload() {
- const fileInput = document.createElement("input");
- fileInput.type = "file";
- fileInput.accept = "image/svg+xml";
- fileInput.style.display = "none";
-
- return fileInput;
-}
-
-function customStampOnClick({ tool, variant, state }) {
- const fileInput = createFileInputForUpload();
-
- function handleFileUpload(event) {
- readUploadedSVG(event, fileInput);
- }
-
- fileInput.addEventListener("stamp-custom-slot-success", async (event) => {
- fileInput.removeEventListener("change", handleFileUpload);
- fileInput.remove();
-
- const updatedVariant = {
- ...variant,
- iconUrl: event.detail.iconDataUri,
- value: event.detail.dataUri,
- };
- updateActivatedButton(
- getPanelToolVariants(),
- updatedVariant.id.description,
- );
- setTool(tool, { state, variant: updatedVariant });
- setCustomVariant(tool, updatedVariant, { state });
- });
- fileInput.addEventListener("stamp-custom-slot-failure", () => {
- alert("Something went wrong with uploading the image!");
- });
- fileInput.addEventListener("change", handleFileUpload);
-
- fileInput.click();
-}
-
-function defaultOnClick({ tool, variant, state }) {
- setTool(tool, { state, variant });
- updateActivatedButton(getPanelToolVariants(), variant.id.description);
-}
-
-function getVariantButtons(container) {
- return Array.from(container.querySelectorAll("button"));
-}
-
// do not bubble event to avoid clicks on canvas
function dispatchButtonClick(element) {
element.dispatchEvent(
@@ -117,26 +25,33 @@ function dispatchButtonClick(element) {
}
function getNextButton(buttons, index) {
- return index + 1 < buttons.length ? buttons[index + 1] : buttons[0];
+ const variantButtons = Array.from(buttons);
+ return index + 1 < buttons.length
+ ? variantButtons[index + 1]
+ : variantButtons[0];
}
function getPreviousButton(buttons, index) {
- return index - 1 >= 0 ? buttons[index - 1] : buttons[buttons.length - 1];
+ const variantButtons = Array.from(buttons);
+ return index - 1 >= 0
+ ? variantButtons[index - 1]
+ : variantButtons[buttons.length - 1];
}
-function getButtonIndex(buttons, { state, tool }) {
+function getButtonIndex(variantButtons, { state, tool }) {
const activatedVariant = state.get((prevState) =>
prevState.activatedVariants.get(tool.id),
);
- const index = buttons.findIndex(
- (button) => button.dataset.value === activatedVariant.id.description,
+ const index = Array.from(variantButtons).findIndex(
+ (variantButton) =>
+ variantButton.button.dataset.value === activatedVariant.id.description,
);
return index;
}
-function attachGamepadListeners(container, tool, { state }) {
+function attachGamepadListeners(tool, { state }) {
let frame = null;
let buttonPressedTimestamp = null;
@@ -150,7 +65,7 @@ function attachGamepadListeners(container, tool, { state }) {
const buttonPressedDelta = timestamp - buttonPressedTimestamp;
- const index = getButtonIndex(getVariantButtons(container), {
+ const index = getButtonIndex(getVariantButtons(), {
state,
tool,
});
@@ -163,14 +78,14 @@ function attachGamepadListeners(container, tool, { state }) {
isLeftTriggerGamepadButtonPressed(gamepad) &&
buttonPressedDelta > GAMEPAD_BUTTON_ACTIVATION_DELAY_IN_MS
) {
- const prevButton = getPreviousButton(getVariantButtons(container), index);
+ const prevButton = getPreviousButton(getVariantButtons(), index);
dispatchButtonClick(prevButton);
buttonPressedTimestamp = timestamp;
} else if (
isRightTriggerGamepadButtonPressed(gamepad) &&
buttonPressedDelta > GAMEPAD_BUTTON_ACTIVATION_DELAY_IN_MS
) {
- const nextButton = getNextButton(getVariantButtons(container), index);
+ const nextButton = getNextButton(getVariantButtons(), index);
dispatchButtonClick(nextButton);
buttonPressedTimestamp = timestamp;
}
@@ -187,20 +102,11 @@ function attachGamepadListeners(container, tool, { state }) {
frame = requestAnimationFrame(activateVariantCycleOnShoulderButtonPresses);
}
- function cancelGamepadAnimationFrame() {
- cancelAnimationFrame(frame);
- frame = null;
- }
-
requestGamepadAnimationFrame();
-
- return function dispose() {
- cancelGamepadAnimationFrame();
- };
}
-function attachKeyboardListeners(container, tool, { state }) {
- const buttons = getVariantButtons(container);
+function attachKeyboardListeners(tool, { state, signal }) {
+ const buttons = getVariantButtons();
function onKeyDown(event) {
const index = getButtonIndex(buttons, { state, tool });
@@ -212,12 +118,12 @@ function attachKeyboardListeners(container, tool, { state }) {
switch (event.key) {
case ".": {
const nextButton = getNextButton(buttons, index);
- dispatchButtonClick(nextButton);
+ dispatchButtonClick(nextButton.button);
break;
}
case ",": {
const prevButton = getPreviousButton(buttons, index);
- dispatchButtonClick(prevButton);
+ dispatchButtonClick(prevButton.button);
break;
}
default:
@@ -225,128 +131,83 @@ function attachKeyboardListeners(container, tool, { state }) {
}
}
- window.addEventListener("keydown", onKeyDown);
-
- return function dispose() {
- window.removeEventListener("keydown", onKeyDown);
- };
+ window.addEventListener("keydown", onKeyDown, {
+ signal,
+ });
}
-function renderToolVariants(tool, state) {
+export function buildToolVariants(tool, { state, signal }) {
const variantsContainer = getPanelToolVariants();
- const listeners = {};
-
- const activatedVariant = state.get((prevState) =>
- prevState.activatedVariants.get(tool.id),
- );
-
- const customVariants = state.get(
- (prevState) => prevState.customVariants.get(tool.id) ?? new Set(),
- );
-
- const allVariants = [...tool.variants, ...customVariants];
-
- allVariants.forEach((variant) => {
- const button = document.createElement("button");
-
- function onClick() {
- switch (tool.id) {
- case TOOLS.STAMP.id:
- {
- if (!variant.value) {
- customStampOnClick({ tool, variant, state });
- } else {
- defaultOnClick({ tool, variant, state });
- }
- }
- break;
- default:
- defaultOnClick({ tool, variant, state });
- break;
- }
+ state.addListener((updatedState, prevState) => {
+ if (
+ prevState.activatedVariants.get(tool.id) ===
+ updatedState.activatedVariants.get(tool.id)
+ ) {
+ return;
}
- button.addEventListener("click", onClick);
-
- button.dataset.value = variant.id.description;
+ const variantButtons = Array.from(getVariantButtons());
+ const prevActiveVariantButton = variantButtons.find((b) => b.isActive);
- if (isDataUri(variant.iconUrl)) {
- button.innerHTML = serializeSvg(
- deserializeSvgFromDataURI(variant.iconUrl),
- );
- } else {
- loadIcon(variant.iconUrl)
- .then((icon) => {
- button.innerHTML = icon;
- })
- .catch((error) => {
- console.error(error);
- button.innerText = variant.id.description;
- });
+ if (prevActiveVariantButton) {
+ prevActiveVariantButton.isActive = false;
}
- variantsContainer.appendChild(button);
+ const nextActiveVariantButton =
+ getVariantButtonById(updatedState.activatedVariants.get(tool.id)?.id) ||
+ variantButtons?.[0];
- if (variant.id === activatedVariant.id) {
- updateActivatedButton(variantsContainer, variant.id.description);
+ if (!nextActiveVariantButton) {
+ throw new Error(
+ `Variant button not found for variant: ${updatedState.tool.activatedVariant.id}`,
+ );
}
- listeners[button.dataset.value] = onClick;
+ nextActiveVariantButton.isActive = true;
});
- const keyboardListenersDisposeCallback = attachKeyboardListeners(
- variantsContainer,
- tool,
- { state },
- );
- const gamepadListenersDisposeCallback = attachGamepadListeners(
- variantsContainer,
- tool,
- { state },
+ const customVariants = state.get(
+ (prevState) => prevState.customVariants.get(tool.id) ?? new Set(),
);
- return function dispose() {
- keyboardListenersDisposeCallback();
- gamepadListenersDisposeCallback();
-
- getVariantButtons(variantsContainer).forEach((button) => {
- disposeCallback(button, listeners);
- button.remove();
- });
-
- ensureCallbacksRemoved(listeners);
-
- variantsContainer.innerHTML = "";
- };
-}
+ const allVariants = [...tool.variants, ...customVariants];
-export function buildToolVariants(tool, state) {
- let disposeVariantsCallback = null;
+ for (const variant of allVariants) {
+ const activatedVariant = state.get((prevState) =>
+ prevState.activatedVariants.get(tool.id),
+ );
- function onToolChange(nextState, prevState) {
- if (nextState.customVariants === prevState.customVariants) {
- return;
- }
+ const options = {
+ ...variant,
+ isActive: activatedVariant?.id.description === variant.id.description,
+ signal,
+ tool,
+ state,
+ variant,
+ };
- if (disposeVariantsCallback) {
- disposeVariantsCallback();
- disposeVariantsCallback = null;
+ switch (tool.id) {
+ case TOOLS.STAMP.id:
+ variantsContainer.appendChild(new VariantStampButton(options));
+ break;
+ default:
+ variantsContainer.appendChild(new VariantButton(options));
+ break;
}
-
- disposeVariantsCallback = renderToolVariants(tool, state);
}
- disposeVariantsCallback = renderToolVariants(tool, state);
+ attachKeyboardListeners(tool, { state, signal });
- state.addListener(onToolChange);
+ attachGamepadListeners(tool, { state });
- return function dispose() {
- state.removeListener(onToolChange);
+ function dispose() {
+ Array.from(getVariantButtons()).forEach((variantButton) => {
+ variantButton.remove();
+ });
- if (disposeVariantsCallback) {
- disposeVariantsCallback();
- disposeVariantsCallback = null;
- }
- };
+ variantsContainer.innerHTML = "";
+ }
+
+ signal.addEventListener("abort", dispose, { once: true });
}
diff --git a/service-worker.js b/service-worker.js
index 1e98fda..059e081 100644
--- a/service-worker.js
+++ b/service-worker.js
@@ -1,5 +1,5 @@
const STATIC_CACHE_NAME = "static";
-const STATIC_CACHE_VERSION = "v42";
+const STATIC_CACHE_VERSION = "v44";
const STATIC_CACHE_ID = `${STATIC_CACHE_NAME}-${STATIC_CACHE_VERSION}`;
// All the files need to be added here manually. I want to avoid
@@ -39,12 +39,17 @@ const STATIC_ASSETS = [
"js/tools/pen.mjs",
"js/tools/stamp.mjs",
"js/ui/actions.mjs",
+ "js/ui/button.mjs",
"js/ui/colors.mjs",
+ "js/ui/color.mjs",
"js/ui/global.mjs",
"js/ui/panel.mjs",
"js/ui/tools.mjs",
+ "js/ui/tool.mjs",
"js/ui/toast.mjs",
"js/ui/utils.mjs",
+ "js/ui/variant.mjs",
+ "js/ui/variant-stamp.mjs",
"js/ui/variants.mjs",
/* CSS */