From d396f0a43d19f74c308ce15bc08c068c4f2242c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Syn=C3=A1=C4=8Dek?= Date: Sun, 30 Mar 2025 21:11:24 +0200 Subject: [PATCH 01/16] convert color buttons to web components --- .eslintrc.js | 1 + css/panel.css | 8 ++++++ js/boot.mjs | 6 +++++ js/ui/color.mjs | 66 +++++++++++++++++++++++++++++++++++++++++++++++ js/ui/colors.mjs | 12 +++------ js/ui/panel.mjs | 9 ++++++- js/ui/utils.mjs | 20 +++++++++++--- service-worker.js | 1 + 8 files changed, 110 insertions(+), 13 deletions(-) create mode 100644 js/ui/color.mjs 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..436fa53 100644 --- a/css/panel.css +++ b/css/panel.css @@ -37,6 +37,14 @@ justify-content: space-between; } +.panel-row color-button { + margin-right: var(--button-space); +} + +.panel-row color-button:last-child { + margin-right: 0px; +} + .panel-row button { margin-right: var(--button-space); width: var(--button-size); diff --git a/js/boot.mjs b/js/boot.mjs index ef6951a..94c2553 100644 --- a/js/boot.mjs +++ b/js/boot.mjs @@ -22,6 +22,11 @@ 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"; + +function registerComponents() { + customElements.define("color-button", ColorButton); +} function attachResizeListeners() { const canvas = getCanvas(); @@ -59,6 +64,7 @@ export function boot() { const state = createState(); const canvas = getCanvas(); + registerComponents(); restorePreviousCanvas(canvas); attachCanvasSaveListener(canvas); diff --git a/js/ui/color.mjs b/js/ui/color.mjs new file mode 100644 index 0000000..cd5b9f9 --- /dev/null +++ b/js/ui/color.mjs @@ -0,0 +1,66 @@ +import { COLOR } from "../state/constants.mjs"; +/** + * Represents a button for selecting colors + */ +export class ColorButton extends HTMLElement { + constructor({ onClick, color }) { + super(); + + this.color = color; + this.#onClick = onClick; + this.attachShadow({ mode: "open" }); + } + + color = "#000000"; + #onClick = () => {}; + + connectedCallback() { + this.shadowRoot.innerHTML = ` + + + `; + + this.#button.addEventListener("click", this.#handleClick); + this.isActive = false; + } + + click(e) { + this.#button.dispatchEvent(e); + } + + set isActive(value) { + if (value) { + this.#button.setAttribute("aria-pressed", "true"); + } else { + this.#button.removeAttribute("aria-pressed", "false"); + } + } + + get #button() { + return this.shadowRoot.querySelector("button"); + } + + #handleClick = (e) => { + this.#onClick(e); + }; +} diff --git a/js/ui/colors.mjs b/js/ui/colors.mjs index 1eac578..e41fa15 100644 --- a/js/ui/colors.mjs +++ b/js/ui/colors.mjs @@ -11,6 +11,7 @@ import { } from "../controls/gamepad.mjs"; import { getPanelColors } from "../dom.mjs"; import { updateActivatedButton } from "./utils.mjs"; +import { ColorButton } from "./color.mjs"; const GAMEPAD_BUTTON_ACTIVATION_DELAY_IN_MS = 300; @@ -122,16 +123,11 @@ export function createColorPanel({ state }) { }); COLOR_LIST.forEach((color) => { - const button = document.createElement("button"); - button.style.backgroundColor = color; - - button.addEventListener("click", () => { - setColor(color, { state }); + const button = new ColorButton({ + color, + onClick: () => setColor(color, { state }), }); - button.dataset.value = color; - button.style.backgroundColor = color; - colors.appendChild(button); }); diff --git a/js/ui/panel.mjs b/js/ui/panel.mjs index 316c0e6..726a28f 100644 --- a/js/ui/panel.mjs +++ b/js/ui/panel.mjs @@ -4,9 +4,10 @@ import { } from "../controls/gamepad.mjs"; import { getPanel } from "../dom.mjs"; import { isCursorWithinPanelBounds } from "./utils.mjs"; +import { ColorButton } from "./color.mjs"; function getPanelButtonByCoordinates(x, y, panel) { - const buttons = panel.querySelectorAll("button"); + const buttons = panel.querySelectorAll("button,color-button"); for (let i = 0; i < buttons.length; i++) { const button = buttons[i]; @@ -43,6 +44,12 @@ function activatePanelButtonOnCoordinates(x, y) { bubbles: false, }); + if (button instanceof ColorButton) { + button.click(clickEvent); + + return; + } + button.dispatchEvent(clickEvent); } diff --git a/js/ui/utils.mjs b/js/ui/utils.mjs index 89b5704..d43b9d8 100644 --- a/js/ui/utils.mjs +++ b/js/ui/utils.mjs @@ -1,3 +1,5 @@ +import { ColorButton } from "./color.mjs"; + export async function loadIcon(url) { const response = await fetch(url); @@ -28,13 +30,23 @@ export function ensureCallbacksRemoved(listeners) { } export function updateActivatedButton(buttonContainer, value) { - const buttons = buttonContainer.querySelectorAll("button"); + const buttons = buttonContainer.querySelectorAll("button,color-button"); Array.from(buttons).forEach((button) => { - if (button.dataset.value === value) { - button.classList.add("active"); + const isColorButton = button instanceof ColorButton; + const buttonColor = isColorButton ? button.color : button.dataset.value; + const isActive = buttonColor === value; + + if (isColorButton) { + button.isActive = isActive; + + return; } else { - button.classList.remove("active"); + if (button.dataset.value === value) { + button.classList.add("active"); + } else { + button.classList.remove("active"); + } } }); } diff --git a/service-worker.js b/service-worker.js index 1e98fda..23cb1e0 100644 --- a/service-worker.js +++ b/service-worker.js @@ -40,6 +40,7 @@ const STATIC_ASSETS = [ "js/tools/stamp.mjs", "js/ui/actions.mjs", "js/ui/colors.mjs", + "js/ui/color.mjs", "js/ui/global.mjs", "js/ui/panel.mjs", "js/ui/tools.mjs", From a4e19e79ad58d8210d79ec24c175f63ad497ddf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Syn=C3=A1=C4=8Dek?= Date: Sun, 30 Mar 2025 22:19:37 +0200 Subject: [PATCH 02/16] convert tool button to web components --- css/panel.css | 24 ++++---------- js/boot.mjs | 2 ++ js/ui/color.mjs | 4 +++ js/ui/panel.mjs | 5 +-- js/ui/tool.mjs | 81 +++++++++++++++++++++++++++++++++++++++++++++++ js/ui/tools.mjs | 27 ++++------------ js/ui/utils.mjs | 32 ++++++++++--------- service-worker.js | 1 + 8 files changed, 121 insertions(+), 55 deletions(-) create mode 100644 js/ui/tool.mjs diff --git a/css/panel.css b/css/panel.css index 436fa53..aa8fffa 100644 --- a/css/panel.css +++ b/css/panel.css @@ -37,29 +37,19 @@ justify-content: space-between; } -.panel-row color-button { +.panel-row color-button, +.panel-row tool-button { margin-right: var(--button-space); } -.panel-row color-button:last-child { +.panel-row color-button:last-child, +.panel-row tool-button:last-child { margin-right: 0px; } -.panel-row 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 { - 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/js/boot.mjs b/js/boot.mjs index 94c2553..8c26a3b 100644 --- a/js/boot.mjs +++ b/js/boot.mjs @@ -23,9 +23,11 @@ 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"; function registerComponents() { customElements.define("color-button", ColorButton); + customElements.define("tool-button", ToolButton); } function attachResizeListeners() { diff --git a/js/ui/color.mjs b/js/ui/color.mjs index cd5b9f9..fc2c092 100644 --- a/js/ui/color.mjs +++ b/js/ui/color.mjs @@ -48,6 +48,10 @@ export class ColorButton extends HTMLElement { this.#button.dispatchEvent(e); } + isColorEqual(color) { + return this.color === color; + } + set isActive(value) { if (value) { this.#button.setAttribute("aria-pressed", "true"); diff --git a/js/ui/panel.mjs b/js/ui/panel.mjs index 726a28f..824cce6 100644 --- a/js/ui/panel.mjs +++ b/js/ui/panel.mjs @@ -5,9 +5,10 @@ import { import { getPanel } from "../dom.mjs"; import { isCursorWithinPanelBounds } from "./utils.mjs"; import { ColorButton } from "./color.mjs"; +import { ToolButton } from "./tool.mjs"; function getPanelButtonByCoordinates(x, y, panel) { - const buttons = panel.querySelectorAll("button,color-button"); + const buttons = panel.querySelectorAll("button,color-button,tool-button"); for (let i = 0; i < buttons.length; i++) { const button = buttons[i]; @@ -44,7 +45,7 @@ function activatePanelButtonOnCoordinates(x, y) { bubbles: false, }); - if (button instanceof ColorButton) { + if (button instanceof ColorButton || button instanceof ToolButton) { button.click(clickEvent); return; diff --git a/js/ui/tool.mjs b/js/ui/tool.mjs new file mode 100644 index 0000000..79ba7f4 --- /dev/null +++ b/js/ui/tool.mjs @@ -0,0 +1,81 @@ +import { loadIcon } from "./utils.mjs"; +/** + * Represents tool/action button. + */ +export class ToolButton extends HTMLElement { + constructor({ id, iconUrl, onClick }) { + super(); + + this.#description = id.description; + this.#iconUrl = iconUrl; + this.id = id; + this.#onClick = onClick; + this.attachShadow({ mode: "open" }); + } + + id = ""; + #iconUrl = ""; + #description = ""; + #onClick = () => {}; + + connectedCallback() { + this.shadowRoot.innerHTML = ` + + + `; + + this.#button.addEventListener("click", this.#handleClick, true); + this.isActive = false; + + loadIcon(this.#iconUrl) + .then((icon) => { + this.#button.innerHTML = icon; + }) + .catch((error) => { + console.error(error); + this.#button.innerText = this.#description; + }); + } + + click(e) { + this.#button.dispatchEvent(e); + } + + set isActive(value) { + if (value) { + this.#button.setAttribute("aria-pressed", "true"); + } else { + this.#button.removeAttribute("aria-pressed", "false"); + } + } + + get #button() { + return this.shadowRoot.querySelector("button"); + } + + #handleClick = (e) => { + this.#onClick(e); + }; + + static compare(id, description) { + return id.description === description; + } +} diff --git a/js/ui/tools.mjs b/js/ui/tools.mjs index 48903a1..25554ce 100644 --- a/js/ui/tools.mjs +++ b/js/ui/tools.mjs @@ -1,9 +1,10 @@ import { getPanelTools } from "../dom.mjs"; import { setTool } from "../state/actions/tool.mjs"; import { TOOL_LIST } from "../state/constants.mjs"; -import { loadIcon, updateActivatedButton } from "./utils.mjs"; +import { 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(); @@ -37,26 +38,10 @@ export function createToolPanel({ state }) { }); TOOL_LIST.forEach((tool) => { - const button = document.createElement("button"); - - button.addEventListener( - "click", - () => { - setTool(tool, { state }); - }, - true, - ); - - button.dataset.value = tool.id.description; - - loadIcon(tool.iconUrl) - .then((icon) => { - button.innerHTML = icon; - }) - .catch((error) => { - console.error(error); - button.innerText = tool.id.description; - }); + const button = new ToolButton({ + ...tool, + onClick: () => setTool(tool, { state }), + }); tools.appendChild(button); }); diff --git a/js/ui/utils.mjs b/js/ui/utils.mjs index d43b9d8..54b4ded 100644 --- a/js/ui/utils.mjs +++ b/js/ui/utils.mjs @@ -1,4 +1,4 @@ -import { ColorButton } from "./color.mjs"; +import { ToolButton } from "./tool.mjs"; export async function loadIcon(url) { const response = await fetch(url); @@ -30,25 +30,27 @@ export function ensureCallbacksRemoved(listeners) { } export function updateActivatedButton(buttonContainer, value) { - const buttons = buttonContainer.querySelectorAll("button,color-button"); + const buttons = buttonContainer.querySelectorAll("button"); Array.from(buttons).forEach((button) => { - const isColorButton = button instanceof ColorButton; - const buttonColor = isColorButton ? button.color : button.dataset.value; - const isActive = buttonColor === value; - - if (isColorButton) { - button.isActive = isActive; - - return; + if (button.dataset.value === value) { + button.classList.add("active"); } else { - if (button.dataset.value === value) { - button.classList.add("active"); - } else { - button.classList.remove("active"); - } + button.classList.remove("active"); } }); + + const colorButtons = buttonContainer.querySelectorAll("color-button"); + + Array.from(colorButtons).forEach((button) => { + button.isActive = button.isColorEqual(value); + }); + + const toolButtons = buttonContainer.querySelectorAll("tool-button"); + + Array.from(toolButtons).forEach((button) => { + button.isActive = ToolButton.compare(button.id, value); + }); } /** diff --git a/service-worker.js b/service-worker.js index 23cb1e0..a026518 100644 --- a/service-worker.js +++ b/service-worker.js @@ -44,6 +44,7 @@ const STATIC_ASSETS = [ "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/variants.mjs", From 69f11e3a780fd76f821c4b363b6ef21c8fd971ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Syn=C3=A1=C4=8Dek?= Date: Mon, 31 Mar 2025 21:44:59 +0200 Subject: [PATCH 03/16] convert variant button to wc --- css/panel.css | 6 ++-- js/boot.mjs | 2 ++ js/ui/panel.mjs | 11 ++++-- js/ui/utils.mjs | 7 ++++ js/ui/variant.mjs | 85 ++++++++++++++++++++++++++++++++++++++++++++++ js/ui/variants.mjs | 50 ++++++++++----------------- service-worker.js | 1 + 7 files changed, 125 insertions(+), 37 deletions(-) create mode 100644 js/ui/variant.mjs diff --git a/css/panel.css b/css/panel.css index aa8fffa..18527c9 100644 --- a/css/panel.css +++ b/css/panel.css @@ -38,12 +38,14 @@ } .panel-row color-button, -.panel-row tool-button { +.panel-row tool-button, +.panel-row variant-button { margin-right: var(--button-space); } .panel-row color-button:last-child, -.panel-row tool-button:last-child { +.panel-row tool-button:last-child, +.panel-row variant-button:last-child { margin-right: 0px; } diff --git a/js/boot.mjs b/js/boot.mjs index 8c26a3b..7a58888 100644 --- a/js/boot.mjs +++ b/js/boot.mjs @@ -24,10 +24,12 @@ 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"; function registerComponents() { customElements.define("color-button", ColorButton); customElements.define("tool-button", ToolButton); + customElements.define("variant-button", VariantButton); } function attachResizeListeners() { diff --git a/js/ui/panel.mjs b/js/ui/panel.mjs index 824cce6..60d3ab0 100644 --- a/js/ui/panel.mjs +++ b/js/ui/panel.mjs @@ -6,9 +6,12 @@ import { getPanel } from "../dom.mjs"; import { isCursorWithinPanelBounds } from "./utils.mjs"; import { ColorButton } from "./color.mjs"; import { ToolButton } from "./tool.mjs"; +import { VariantButton } from "./variant.mjs"; function getPanelButtonByCoordinates(x, y, panel) { - const buttons = panel.querySelectorAll("button,color-button,tool-button"); + const buttons = panel.querySelectorAll( + "button,color-button,tool-button,variant-button", + ); for (let i = 0; i < buttons.length; i++) { const button = buttons[i]; @@ -45,7 +48,11 @@ function activatePanelButtonOnCoordinates(x, y) { bubbles: false, }); - if (button instanceof ColorButton || button instanceof ToolButton) { + if ( + button instanceof ColorButton || + button instanceof ToolButton || + button instanceof VariantButton + ) { button.click(clickEvent); return; diff --git a/js/ui/utils.mjs b/js/ui/utils.mjs index 54b4ded..f528a42 100644 --- a/js/ui/utils.mjs +++ b/js/ui/utils.mjs @@ -1,4 +1,5 @@ import { ToolButton } from "./tool.mjs"; +import { VariantButton } from "./variant.mjs"; export async function loadIcon(url) { const response = await fetch(url); @@ -51,6 +52,12 @@ export function updateActivatedButton(buttonContainer, value) { Array.from(toolButtons).forEach((button) => { button.isActive = ToolButton.compare(button.id, value); }); + + const variantButtons = buttonContainer.querySelectorAll("variant-button"); + + Array.from(variantButtons).forEach((button) => { + button.isActive = VariantButton.compare(button.id, value); + }); } /** diff --git a/js/ui/variant.mjs b/js/ui/variant.mjs new file mode 100644 index 0000000..343fa8a --- /dev/null +++ b/js/ui/variant.mjs @@ -0,0 +1,85 @@ +import { isDataUri } from "../state/utils.mjs"; +import { loadIcon } from "./utils.mjs"; +import { serializeSvg, deserializeSvgFromDataURI } from "../svg-utils.mjs"; + +export class VariantButton extends HTMLElement { + constructor({ id, onClick, iconUrl }) { + super(); + + this.id = id; + this.#onClick = onClick; + this.#iconUrl = iconUrl; + + this.attachShadow({ mode: "open" }); + } + + id = ""; + #onClick = () => {}; + #iconUrl = ""; + + connectedCallback() { + this.shadowRoot.innerHTML = ` + + + `; + + this.#button.addEventListener("click", this.#handleClick, true); + this.isActive = false; + + if (isDataUri(this.#iconUrl)) { + this.#button.innerHTML = serializeSvg( + deserializeSvgFromDataURI(this.#iconUrl), + ); + } else { + loadIcon(this.#iconUrl) + .then((icon) => { + this.#button.innerHTML = icon; + }) + .catch((error) => { + console.error(error); + this.#button.innerText = this.id.description; + }); + } + } + + click(e) { + this.#button.dispatchEvent(e); + } + + get #button() { + return this.shadowRoot.querySelector("button"); + } + + get button() { + return this.#button; + } + + set isActive(value) { + this.#button.setAttribute("aria-pressed", value ? "true" : "false"); + } + + #handleClick = (event) => { + this.#onClick(event); + }; + + static compare(id, value) { + return id.description === value; + } +} diff --git a/js/ui/variants.mjs b/js/ui/variants.mjs index 1889418..0075211 100644 --- a/js/ui/variants.mjs +++ b/js/ui/variants.mjs @@ -1,6 +1,5 @@ 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 { isLeftTriggerGamepadButtonPressed, @@ -8,7 +7,6 @@ import { getGamepad, } from "../controls/gamepad.mjs"; import { - loadIcon, disposeCallback, ensureCallbacksRemoved, updateActivatedButton, @@ -19,6 +17,7 @@ import { deserializeSvgFromDataURI, normalizeSvgSize, } from "../svg-utils.mjs"; +import { VariantButton } from "./variant.mjs"; const GAMEPAD_BUTTON_ACTIVATION_DELAY_IN_MS = 300; @@ -103,7 +102,7 @@ function defaultOnClick({ tool, variant, state }) { } function getVariantButtons(container) { - return Array.from(container.querySelectorAll("button")); + return Array.from(container.querySelectorAll("button,variant-button")); } // do not bubble event to avoid clicks on canvas @@ -124,13 +123,14 @@ function getPreviousButton(buttons, index) { return index - 1 >= 0 ? buttons[index - 1] : buttons[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 = variantButtons.findIndex( + (variantButton) => + variantButton.button.dataset.value === activatedVariant.id.description, ); return index; @@ -212,12 +212,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: @@ -248,8 +248,6 @@ function renderToolVariants(tool, state) { const allVariants = [...tool.variants, ...customVariants]; allVariants.forEach((variant) => { - const button = document.createElement("button"); - function onClick() { switch (tool.id) { case TOOLS.STAMP.id: @@ -267,32 +265,18 @@ function renderToolVariants(tool, state) { } } - button.addEventListener("click", onClick); - - button.dataset.value = variant.id.description; - - 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; - }); - } + const variantButton = new VariantButton({ + ...variant, + onClick, + }); - variantsContainer.appendChild(button); + variantsContainer.appendChild(variantButton); if (variant.id === activatedVariant.id) { updateActivatedButton(variantsContainer, variant.id.description); } - listeners[button.dataset.value] = onClick; + listeners[variantButton.button.dataset.value] = onClick; }); const keyboardListenersDisposeCallback = attachKeyboardListeners( @@ -310,9 +294,9 @@ function renderToolVariants(tool, state) { keyboardListenersDisposeCallback(); gamepadListenersDisposeCallback(); - getVariantButtons(variantsContainer).forEach((button) => { - disposeCallback(button, listeners); - button.remove(); + getVariantButtons(variantsContainer).forEach((variantButton) => { + disposeCallback(variantButton.button, listeners); + variantButton.remove(); }); ensureCallbacksRemoved(listeners); diff --git a/service-worker.js b/service-worker.js index a026518..ef16621 100644 --- a/service-worker.js +++ b/service-worker.js @@ -47,6 +47,7 @@ const STATIC_ASSETS = [ "js/ui/tool.mjs", "js/ui/toast.mjs", "js/ui/utils.mjs", + "js/ui/variant.mjs", "js/ui/variants.mjs", /* CSS */ From fccf0f239f121a7ea2ecfb61345df112133703b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Syn=C3=A1=C4=8Dek?= Date: Mon, 31 Mar 2025 22:31:03 +0200 Subject: [PATCH 04/16] create base button component and re-use it --- css/panel.css | 2 + js/boot.mjs | 2 + js/ui/button.mjs | 98 +++++++++++++++++++++++++++++++++++++++++++++++ js/ui/color.mjs | 59 +++++++++------------------- js/ui/tool.mjs | 60 ++++++++--------------------- js/ui/variant.mjs | 61 +++++++---------------------- 6 files changed, 149 insertions(+), 133 deletions(-) create mode 100644 js/ui/button.mjs diff --git a/css/panel.css b/css/panel.css index 18527c9..f07c9e8 100644 --- a/css/panel.css +++ b/css/panel.css @@ -37,12 +37,14 @@ justify-content: space-between; } +.panel-row ui-button, .panel-row color-button, .panel-row tool-button, .panel-row variant-button { margin-right: var(--button-space); } +.panel-row ui-button:last-child, .panel-row color-button:last-child, .panel-row tool-button:last-child, .panel-row variant-button:last-child { diff --git a/js/boot.mjs b/js/boot.mjs index 7a58888..44e0491 100644 --- a/js/boot.mjs +++ b/js/boot.mjs @@ -25,8 +25,10 @@ import { initializeCursor } from "./cursor.mjs"; import { ColorButton } from "./ui/color.mjs"; import { ToolButton } from "./ui/tool.mjs"; import { VariantButton } from "./ui/variant.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); diff --git a/js/ui/button.mjs b/js/ui/button.mjs new file mode 100644 index 0000000..aaf5462 --- /dev/null +++ b/js/ui/button.mjs @@ -0,0 +1,98 @@ +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 { + constructor({ ariaLabel, dataset, iconUrl, backgroundColor, onClick }) { + super(); + + this.#onClick = onClick; + this.#ariaLabel = ariaLabel; + this.#dataset = dataset; + this.#iconUrl = iconUrl; + this.#backgroundColor = backgroundColor; + + this.attachShadow({ mode: "open" }); + } + + #ariaLabel = ""; + #dataset = {}; + #iconUrl = ""; + #backgroundColor = "#000000"; + #onClick = () => {}; + + connectedCallback() { + this.shadowRoot.innerHTML = ` + + + `; + + this.button.addEventListener("click", this.#onClick); + this.isActive = false; + this.#setContents(); + } + + #setContents() { + if (!this.#iconUrl) { + return; + } + + if (isDataUri(this.#iconUrl)) { + this.button.innerHTML = serializeSvg( + deserializeSvgFromDataURI(this.#iconUrl), + ); + } else { + loadIcon(this.#iconUrl) + .then((icon) => { + this.button.innerHTML = icon; + }) + .catch((error) => { + console.error(error); + this.button.innerText = UiButton.#getContentFallback(this.#dataset); + }); + } + } + + get button() { + return this.shadowRoot.querySelector("button"); + } + + set isActive(value) { + this.button.setAttribute("aria-pressed", value ? "true" : "false"); + } + + 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 index fc2c092..844e663 100644 --- a/js/ui/color.mjs +++ b/js/ui/color.mjs @@ -1,4 +1,6 @@ import { COLOR } from "../state/constants.mjs"; +import { UiButton } from "./button.mjs"; + /** * Represents a button for selecting colors */ @@ -15,56 +17,33 @@ export class ColorButton extends HTMLElement { #onClick = () => {}; connectedCallback() { - this.shadowRoot.innerHTML = ` - - - `; - - this.#button.addEventListener("click", this.#handleClick); - this.isActive = false; + 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, + }); + + this.shadowRoot.appendChild(button); } click(e) { this.#button.dispatchEvent(e); } - isColorEqual(color) { - return this.color === color; - } - set isActive(value) { - if (value) { - this.#button.setAttribute("aria-pressed", "true"); - } else { - this.#button.removeAttribute("aria-pressed", "false"); - } + this.shadowRoot.querySelector("ui-button").isActive = value; } get #button() { - return this.shadowRoot.querySelector("button"); + return this.shadowRoot.querySelector("ui-button").button; } - #handleClick = (e) => { - this.#onClick(e); - }; + isColorEqual(color) { + return this.color === color; + } } diff --git a/js/ui/tool.mjs b/js/ui/tool.mjs index 79ba7f4..3dfa66e 100644 --- a/js/ui/tool.mjs +++ b/js/ui/tool.mjs @@ -1,4 +1,5 @@ -import { loadIcon } from "./utils.mjs"; +import { UiButton } from "./button.mjs"; + /** * Represents tool/action button. */ @@ -19,40 +20,17 @@ export class ToolButton extends HTMLElement { #onClick = () => {}; connectedCallback() { - this.shadowRoot.innerHTML = ` - - - `; - - this.#button.addEventListener("click", this.#handleClick, true); - this.isActive = false; - - loadIcon(this.#iconUrl) - .then((icon) => { - this.#button.innerHTML = icon; - }) - .catch((error) => { - console.error(error); - this.#button.innerText = this.#description; - }); + const button = new UiButton({ + ariaLabel: this.#description, + dataset: { + id: this.id.description, + value: this.#description, + }, + iconUrl: this.#iconUrl, + onClick: this.#onClick, + }); + + this.shadowRoot.appendChild(button); } click(e) { @@ -60,21 +38,13 @@ export class ToolButton extends HTMLElement { } set isActive(value) { - if (value) { - this.#button.setAttribute("aria-pressed", "true"); - } else { - this.#button.removeAttribute("aria-pressed", "false"); - } + this.shadowRoot.querySelector("ui-button").isActive = value; } get #button() { - return this.shadowRoot.querySelector("button"); + return this.shadowRoot.querySelector("ui-button").button; } - #handleClick = (e) => { - this.#onClick(e); - }; - static compare(id, description) { return id.description === description; } diff --git a/js/ui/variant.mjs b/js/ui/variant.mjs index 343fa8a..862411c 100644 --- a/js/ui/variant.mjs +++ b/js/ui/variant.mjs @@ -1,6 +1,4 @@ -import { isDataUri } from "../state/utils.mjs"; -import { loadIcon } from "./utils.mjs"; -import { serializeSvg, deserializeSvgFromDataURI } from "../svg-utils.mjs"; +import { UiButton } from "./button.mjs"; export class VariantButton extends HTMLElement { constructor({ id, onClick, iconUrl }) { @@ -18,45 +16,16 @@ export class VariantButton extends HTMLElement { #iconUrl = ""; connectedCallback() { - this.shadowRoot.innerHTML = ` - - - `; - - this.#button.addEventListener("click", this.#handleClick, true); - this.isActive = false; - - if (isDataUri(this.#iconUrl)) { - this.#button.innerHTML = serializeSvg( - deserializeSvgFromDataURI(this.#iconUrl), - ); - } else { - loadIcon(this.#iconUrl) - .then((icon) => { - this.#button.innerHTML = icon; - }) - .catch((error) => { - console.error(error); - this.#button.innerText = this.id.description; - }); - } + const button = new UiButton({ + ariaLabel: `${this.id.description.toLocaleLowerCase()} variant}`, + dataset: { + value: this.id.description, + }, + iconUrl: this.#iconUrl, + onClick: this.#onClick, + }); + + this.shadowRoot.appendChild(button); } click(e) { @@ -64,7 +33,7 @@ export class VariantButton extends HTMLElement { } get #button() { - return this.shadowRoot.querySelector("button"); + return this.shadowRoot.querySelector("ui-button").button; } get button() { @@ -72,13 +41,9 @@ export class VariantButton extends HTMLElement { } set isActive(value) { - this.#button.setAttribute("aria-pressed", value ? "true" : "false"); + this.shadowRoot.querySelector("ui-button").isActive = value; } - #handleClick = (event) => { - this.#onClick(event); - }; - static compare(id, value) { return id.description === value; } From 51c2ce48281c72a63b65973ddcb31795ae7f600c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Syn=C3=A1=C4=8Dek?= Date: Mon, 31 Mar 2025 22:47:47 +0200 Subject: [PATCH 05/16] re-use UI button web component for main UI --- index.html | 6 ++--- js/ui/button.mjs | 58 +++++++++++++++++++++++++++++++++--------------- js/ui/global.mjs | 12 +++++----- js/ui/panel.mjs | 4 +++- 4 files changed, 52 insertions(+), 28 deletions(-) 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/ui/button.mjs b/js/ui/button.mjs index aaf5462..55d6524 100644 --- a/js/ui/button.mjs +++ b/js/ui/button.mjs @@ -6,9 +6,14 @@ import { isDataUri } from "../state/utils.mjs"; * This is a base UI button with square shape. */ export class UiButton extends HTMLElement { - constructor({ ariaLabel, dataset, iconUrl, backgroundColor, onClick }) { + // Can be built also declaratively with HTML. + constructor(options = {}) { + const { id, ariaLabel, dataset, iconUrl, backgroundColor, onClick } = + options; + super(); + this.#id = id; this.#onClick = onClick; this.#ariaLabel = ariaLabel; this.#dataset = dataset; @@ -18,6 +23,7 @@ export class UiButton extends HTMLElement { this.attachShadow({ mode: "open" }); } + #id = ""; #ariaLabel = ""; #dataset = {}; #iconUrl = ""; @@ -45,6 +51,7 @@ export class UiButton extends HTMLElement { }