From afd474ba07ab0d49d8a2a6714f5368c6e42a9a83 Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Mon, 22 Aug 2022 15:50:07 +0200 Subject: [PATCH] Overhauled the trophy badges This commit includes a few different changes that are tied together: (1) Changed the icon data format from `\0` to `;`, because browser refuse to submit null bytes. (2) Completely rewrote the badge editor to use inline buttons instead of a dialog (avoids bad a11y caused by dialogs in dialogs). --- com.woltlab.wcf/templates/trophyBadge.tpl | 8 +- ts/WoltLabSuite/Core/Acp/Ui/Trophy/Badge.ts | 238 ++++-------------- ts/WoltLabSuite/Core/Acp/Ui/Trophy/Editor.ts | 63 +++++ ts/WoltLabSuite/Core/Ui/Color/Picker.ts | 12 +- .../install/files/acp/templates/trophyAdd.tpl | 56 ++--- .../files/acp/templates/trophyBadge.tpl | 6 +- .../WoltLabSuite/Core/Acp/Ui/Trophy/Badge.js | 199 ++++----------- .../WoltLabSuite/Core/Acp/Ui/Trophy/Editor.js | 59 +++++ .../js/WoltLabSuite/Core/Ui/Color/Picker.js | 14 +- .../lib/acp/form/TrophyAddForm.class.php | 10 +- .../data/acp/menu/item/ACPMenuItem.class.php | 2 +- .../files/lib/data/trophy/Trophy.class.php | 14 ++ ...ACPMenuPackageInstallationPlugin.class.php | 2 +- .../system/style/FontAwesomeIcon.class.php | 14 +- .../exception/InvalidIconFormat.class.php | 2 +- ...jectActionFunctionTemplatePlugin.class.php | 16 +- .../install/files/style/ui/listSortable.scss | 6 +- wcfsetup/install/files/style/ui/trophy.scss | 25 +- wcfsetup/install/lang/de.xml | 2 + wcfsetup/install/lang/en.xml | 2 + 20 files changed, 313 insertions(+), 437 deletions(-) create mode 100644 ts/WoltLabSuite/Core/Acp/Ui/Trophy/Editor.ts create mode 100644 wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/Trophy/Editor.js diff --git a/com.woltlab.wcf/templates/trophyBadge.tpl b/com.woltlab.wcf/templates/trophyBadge.tpl index 884fa4fb8e..7eb5a00f9c 100644 --- a/com.woltlab.wcf/templates/trophyBadge.tpl +++ b/com.woltlab.wcf/templates/trophyBadge.tpl @@ -1,6 +1,8 @@ - +> + {@$trophy->getIcon()->toHtml($size)} + diff --git a/ts/WoltLabSuite/Core/Acp/Ui/Trophy/Badge.ts b/ts/WoltLabSuite/Core/Acp/Ui/Trophy/Badge.ts index 4bcfcbb386..fd6bdd725e 100644 --- a/ts/WoltLabSuite/Core/Acp/Ui/Trophy/Badge.ts +++ b/ts/WoltLabSuite/Core/Acp/Ui/Trophy/Badge.ts @@ -1,205 +1,63 @@ /** - * Provides the trophy icon designer. + * Enables editing of the badge icon, color and + * background-color. * - * @author Joshua Ruesweg - * @copyright 2001-2019 WoltLab GmbH - * @license GNU Lesser General Public License - * @module WoltLabSuite/Core/Acp/Ui/Trophy/Badge + * @author Alexander Ebert + * @copyright 2001-2022 WoltLab GmbH + * @license GNU Lesser General Public License + * @module WoltLabSuite/Core/Acp/Ui/Trophy/Badge */ -import * as Language from "../../../Language"; -import UiDialog from "../../../Ui/Dialog"; -import * as UiStyleFontAwesome from "../../../Ui/Style/FontAwesome"; -import { DialogCallbackObject, DialogCallbackSetup } from "../../../Ui/Dialog/Data"; +import { open as openFontAwesomePicker } from "../../../Ui/Style/FontAwesome"; import ColorPicker from "../../../Ui/Color/Picker"; - -interface Rgba { - r: number; - g: number; - b: number; - a: number; +import DomUtil from "../../../Dom/Util"; + +const badgeContainer = document.getElementById("badgeContainer")!; +const previewWrapper = badgeContainer.querySelector(".trophyIcon") as HTMLElement; +const previewIcon = previewWrapper.querySelector("fa-icon")!; + +function setupChangeIcon(): void { + const button = badgeContainer.querySelector('.trophyIconEditButton[data-value="icon"]') as HTMLButtonElement; + const input = badgeContainer.querySelector('input[name="iconName"]') as HTMLInputElement; + + button.addEventListener("click", () => { + openFontAwesomePicker((icon, forceSolid) => { + previewIcon.setIcon(icon, forceSolid); + input.value = `${icon};${String(forceSolid)}`; + }); + }); } -type Color = string | Rgba; - -/** - * @exports WoltLabSuite/Core/Acp/Ui/Trophy/Badge - */ -class AcpUiTrophyBadge implements DialogCallbackObject { - private badgeColor?: HTMLSpanElement = undefined; - private readonly badgeColorInput: HTMLInputElement; - private dialogContent?: HTMLElement = undefined; - private icon?: HTMLSpanElement = undefined; - private iconColor?: HTMLSpanElement = undefined; - private readonly iconColorInput: HTMLInputElement; - private readonly iconNameInput: HTMLInputElement; - - /** - * Initializes the badge designer. - */ - constructor() { - const iconContainer = document.getElementById("badgeContainer")!; - const button = iconContainer.querySelector(".button") as HTMLElement; - button.addEventListener("click", (ev) => this.click(ev)); - - this.iconNameInput = iconContainer.querySelector('input[name="iconName"]') as HTMLInputElement; - this.iconColorInput = iconContainer.querySelector('input[name="iconColor"]') as HTMLInputElement; - this.badgeColorInput = iconContainer.querySelector('input[name="badgeColor"]') as HTMLInputElement; - } - - /** - * Opens the icon designer. - */ - private click(event: MouseEvent): void { - event.preventDefault(); - - UiDialog.open(this); - } - - /** - * Sets the icon name. - */ - private setIcon(iconName: string): void { - this.icon!.textContent = iconName; - - this.renderIcon(); - } - - /** - * Sets the icon color, can be either a string or an object holding the - * individual r, g, b and a values. - */ - private setIconColor(color: Color): void { - if (typeof color !== "string") { - color = `rgba(${color.r}, ${color.g}, ${color.b}, ${color.a})`; - } - - this.iconColor!.dataset.color = color; - this.iconColor!.style.setProperty("background-color", color, ""); - - this.renderIcon(); - } - - /** - * Sets the badge color, can be either a string or an object holding the - * individual r, g, b and a values. - */ - private setBadgeColor(color: Color): void { - if (typeof color !== "string") { - color = `rgba(${color.r}, ${color.g}, ${color.b}, ${color.a})`; - } - - this.badgeColor!.dataset.color = color; - this.badgeColor!.style.setProperty("background-color", color, ""); - - this.renderIcon(); - } - - /** - * Renders the custom icon preview. - */ - private renderIcon(): void { - const iconColor = this.iconColor!.style.getPropertyValue("background-color"); - const badgeColor = this.badgeColor!.style.getPropertyValue("background-color"); - - const icon = this.dialogContent!.querySelector(".jsTrophyIcon") as HTMLElement; +function setupChangeColor(): void { + const button = badgeContainer.querySelector('.trophyIconEditButton[data-value="color"]') as HTMLButtonElement; - // set icon - icon.className = icon.className.replace(/\b(fa-[a-z0-9-]+)\b/, ""); - icon.classList.add(`fa-${this.icon!.textContent!}`); + const input = badgeContainer.querySelector('input[name="iconColor"]') as HTMLInputElement; + button.dataset.store = DomUtil.identify(input); - icon.style.setProperty("color", iconColor, ""); - icon.style.setProperty("background-color", badgeColor, ""); - } - - /** - * Saves the custom icon design. - */ - private save(event: MouseEvent): void { - event.preventDefault(); - - const iconColor = this.iconColor!.style.getPropertyValue("background-color"); - const badgeColor = this.badgeColor!.style.getPropertyValue("background-color"); - const icon = this.icon!.textContent!; - - this.iconNameInput.value = icon; - this.badgeColorInput.value = badgeColor; - this.iconColorInput.value = iconColor; - - const badgeContainer = document.getElementById("badgeContainer")!; - const previewIcon = badgeContainer.querySelector(".jsTrophyIcon") as HTMLElement; - - // set icon - previewIcon.className = previewIcon.className.replace(/\b(fa-[a-z0-9-]+)\b/, ""); - // TODO: FA6 - previewIcon.classList.add("fa-" + icon); - previewIcon.style.setProperty("color", iconColor, ""); - previewIcon.style.setProperty("background-color", badgeColor, ""); - - UiDialog.close(this); - } - - _dialogSetup(): ReturnType { - return { - id: "trophyIconEditor", - options: { - onSetup: (context) => { - this.dialogContent = context; - - this.iconColor = context.querySelector("#jsIconColorContainer .colorBoxValue") as HTMLSpanElement; - this.badgeColor = context.querySelector("#jsBadgeColorContainer .colorBoxValue") as HTMLSpanElement; - this.icon = context.querySelector(".jsTrophyIconName") as HTMLSpanElement; - - const buttonIconPicker = context.querySelector(".jsTrophyIconName + .button") as HTMLAnchorElement; - buttonIconPicker.addEventListener("click", (event) => { - event.preventDefault(); - - UiStyleFontAwesome.open((iconName) => this.setIcon(iconName)); - }); - - const iconColorContainer = document.getElementById("jsIconColorContainer")!; - const iconColorPicker = iconColorContainer.querySelector(".jsButtonIconColorPicker") as HTMLAnchorElement; - iconColorPicker.addEventListener("click", (event) => { - event.preventDefault(); - - const picker = iconColorContainer.querySelector(".jsColorPicker") as HTMLAnchorElement; - picker.click(); - }); - - const badgeColorContainer = document.getElementById("jsBadgeColorContainer")!; - const badgeColorPicker = badgeColorContainer.querySelector(".jsButtonBadgeColorPicker") as HTMLAnchorElement; - badgeColorPicker.addEventListener("click", (event) => { - event.preventDefault(); + new ColorPicker(button, { + callbackSubmit() { + previewWrapper.style.setProperty("color", input.value); + }, + }); +} - const picker = badgeColorContainer.querySelector(".jsColorPicker") as HTMLAnchorElement; - picker.click(); - }); +function setupChangeBackgroundColor(): void { + const button = badgeContainer.querySelector( + '.trophyIconEditButton[data-value="background-color"]', + ) as HTMLButtonElement; - document.querySelectorAll(".jsColorPicker").forEach((element: HTMLElement) => { - new ColorPicker(element, { callbackSubmit: () => this.renderIcon() }); - }); + const input = badgeContainer.querySelector('input[name="badgeColor"]') as HTMLInputElement; + button.dataset.store = DomUtil.identify(input); - const submitButton = context.querySelector(".formSubmit > .buttonPrimary") as HTMLElement; - submitButton.addEventListener("click", (ev) => this.save(ev)); - }, - onShow: () => { - this.setIcon(this.iconNameInput.value); - this.setIconColor(this.iconColorInput.value); - this.setBadgeColor(this.badgeColorInput.value); - }, - title: Language.get("wcf.acp.trophy.badge.edit"), - }, - }; - } + new ColorPicker(button, { + callbackSubmit() { + previewWrapper.style.setProperty("background-color", input.value); + }, + }); } -let acpUiTrophyBadge: AcpUiTrophyBadge; - -/** - * Initializes the badge designer. - */ -export function init(): void { - if (!acpUiTrophyBadge) { - acpUiTrophyBadge = new AcpUiTrophyBadge(); - } +export function setup(): void { + setupChangeIcon(); + setupChangeColor(); + setupChangeBackgroundColor(); } diff --git a/ts/WoltLabSuite/Core/Acp/Ui/Trophy/Editor.ts b/ts/WoltLabSuite/Core/Acp/Ui/Trophy/Editor.ts new file mode 100644 index 0000000000..d03d43e6fe --- /dev/null +++ b/ts/WoltLabSuite/Core/Acp/Ui/Trophy/Editor.ts @@ -0,0 +1,63 @@ +/** + * Switches between trophy types, automatic awarding of + * trophies and initialized the badge editor. + * + * @author Alexander Ebert + * @copyright 2001-2022 WoltLab GmbH + * @license GNU Lesser General Public License + * @module WoltLabSuite/Core/Acp/Ui/Trophy/Editor + */ + +import { setup as setupBadgeEditor } from "./Badge"; + +const enum TrophyType { + Image = "1", + Badge = "2", +} + +function setupTypeChange(): void { + const badgeContainer = document.getElementById("badgeContainer")!; + const imageContainer = document.getElementById("imageContainer")!; + + const typeSelection = document.querySelector("select[name=type]") as HTMLSelectElement; + typeSelection.addEventListener("change", () => { + if (typeSelection.value === TrophyType.Image) { + badgeContainer.hidden = true; + imageContainer.hidden = false; + } else if (typeSelection.value === TrophyType.Badge) { + badgeContainer.hidden = false; + imageContainer.hidden = true; + } + }); +} + +function setupAwardConditions(): void { + const awardAutomatically = document.querySelector("input[name=awardAutomatically]") as HTMLInputElement; + const revokeContainer = document.getElementById("revokeAutomaticallyDL")!; + const revokeCheckbox = revokeContainer.querySelector("input")!; + + awardAutomatically.addEventListener("change", () => { + document.querySelectorAll(".conditionSection").forEach((section: HTMLElement) => { + if (awardAutomatically.checked) { + section.hidden = false; + } else { + section.hidden = true; + } + }); + + if (awardAutomatically) { + revokeContainer.classList.remove("disabled"); + revokeCheckbox.disabled = false; + } else { + revokeContainer.classList.add("disabled"); + revokeCheckbox.disabled = true; + revokeCheckbox.checked = false; + } + }); +} + +export function setup(): void { + setupTypeChange(); + setupAwardConditions(); + setupBadgeEditor(); +} diff --git a/ts/WoltLabSuite/Core/Ui/Color/Picker.ts b/ts/WoltLabSuite/Core/Ui/Color/Picker.ts index 64b4de7dcb..8be287c50d 100644 --- a/ts/WoltLabSuite/Core/Ui/Color/Picker.ts +++ b/ts/WoltLabSuite/Core/Ui/Color/Picker.ts @@ -342,11 +342,13 @@ class UiColorPicker implements DialogCallbackObject { this.oldColor!.style.backgroundColor = colorString; this.input.value = colorString; - const span = this.element.querySelector("span"); - if (span) { - span.style.backgroundColor = colorString; - } else { - this.element.style.backgroundColor = colorString; + if (!(this.element instanceof HTMLButtonElement)) { + const span = this.element.querySelector("span"); + if (span) { + span.style.backgroundColor = colorString; + } else { + this.element.style.backgroundColor = colorString; + } } UiDialog.close(this); diff --git a/wcfsetup/install/files/acp/templates/trophyAdd.tpl b/wcfsetup/install/files/acp/templates/trophyAdd.tpl index 8440f72abe..88fa699a45 100644 --- a/wcfsetup/install/files/acp/templates/trophyAdd.tpl +++ b/wcfsetup/install/files/acp/templates/trophyAdd.tpl @@ -4,7 +4,7 @@ {include file='fontAwesomeJavaScript'} @@ -167,7 +140,7 @@ {event name='dataFields'} -