From f5490cb5bca088d1c56f2493a160a47cd7c673b5 Mon Sep 17 00:00:00 2001 From: Matthias Schmidt Date: Thu, 1 Jul 2021 09:58:43 +0200 Subject: [PATCH] Update `Ui/Color/Picker` to use native TypeScript-based implementation Instead of having to implement a color picker ourselves, we delegate the task to the browser by using a `input[type=color]` element. Because this color input does not support transparency, there is an additional `input[type=range]` element for transparency. --- ts/WoltLabSuite/Core/Ui/Color/Picker.ts | 274 +++++++++++++++--- .../js/WoltLabSuite/Core/Ui/Color/Picker.js | 228 +++++++++++++-- .../install/files/style/ui/colorPicker.scss | 42 +++ wcfsetup/install/lang/de.xml | 5 + wcfsetup/install/lang/en.xml | 5 + 5 files changed, 474 insertions(+), 80 deletions(-) diff --git a/ts/WoltLabSuite/Core/Ui/Color/Picker.ts b/ts/WoltLabSuite/Core/Ui/Color/Picker.ts index 6797ad5f80..89eeb1979e 100644 --- a/ts/WoltLabSuite/Core/Ui/Color/Picker.ts +++ b/ts/WoltLabSuite/Core/Ui/Color/Picker.ts @@ -2,63 +2,38 @@ * Wrapper class to provide color picker support. Constructing a new object does not * guarantee the picker to be ready at the time of call. * - * @author Alexander Ebert - * @copyright 2001-2019 WoltLab GmbH + * @author Alexander Ebert, Matthias Schmidt + * @copyright 2001-2021 WoltLab GmbH * @license GNU Lesser General Public License * @module WoltLabSuite/Core/Ui/Color/Picker * @woltlabExcludeBundle all */ import * as Core from "../../Core"; +import UiDialog from "../Dialog"; +import { DialogCallbackObject, DialogCallbackSetup } from "../Dialog/Data"; +import DomUtil from "../../Dom/Util"; +import * as Language from "../../Language"; +import * as ColorUtil from "../../ColorUtil"; -let _marshal = (element: HTMLElement, options: ColorPickerOptions) => { - if (typeof window.WCF === "object" && typeof window.WCF.ColorPicker === "function") { - _marshal = (element, options) => { - const picker = new window.WCF.ColorPicker(element); - - if (typeof options.callbackSubmit === "function") { - picker.setCallbackSubmit(options.callbackSubmit); - } - - return picker; - }; - - return _marshal(element, options); - } else { - if (_queue.length === 0) { - window.__wcf_bc_colorPickerInit = () => { - _queue.forEach((data) => { - _marshal(data[0], data[1]); - }); - - window.__wcf_bc_colorPickerInit = undefined; - _queue = []; - }; - } - - _queue.push([element, options]); - } -}; - -type QueueItem = [HTMLElement, ColorPickerOptions]; - -let _queue: QueueItem[] = []; - -interface CallbackSubmitPayload { - r: number; - g: number; - b: number; - a: number; -} +type CallbackSubmit = (data: ColorUtil.RGBA) => void; interface ColorPickerOptions { - callbackSubmit: (data: CallbackSubmitPayload) => void; + callbackSubmit: CallbackSubmit; } -class UiColorPicker { +class UiColorPicker implements DialogCallbackObject { + protected alphaInput: HTMLInputElement | null = null; + protected colorInput: HTMLInputElement | null = null; + protected colorTextInput: HTMLInputElement | null = null; + protected readonly element: HTMLElement; + protected readonly input: HTMLInputElement; + protected newColor: HTMLSpanElement | null = null; + protected oldColor: HTMLSpanElement | null = null; + protected readonly options: ColorPickerOptions; + /** - * Initializes a new color picker instance. This is actually just a wrapper that does - * not guarantee the picker to be ready at the time of call. + * Initializes a new color picker instance. */ constructor(element: HTMLElement, options?: Partial) { if (!(element instanceof Element)) { @@ -67,14 +42,217 @@ class UiColorPicker { ); } - options = Core.extend( + this.element = element; + this.input = document.getElementById(element.dataset.store!) as HTMLInputElement; + if (!this.input) { + throw new Error(`Cannot find input element for color picker ${DomUtil.identify(element)}.`); + } + + this.options = Core.extend( { callbackSubmit: null, }, options || {}, - ); + ) as ColorPickerOptions; + + element.addEventListener("click", () => this.openPicker()); + } + + public _dialogSetup(): ReturnType { + return { + id: `${DomUtil.identify(this.element)}_colorPickerDialog`, + source: ` +
+
+
+
+
${Language.get("wcf.style.colorPicker.color")}
+
+ +
+
+
+
${Language.get("wcf.style.colorPicker.alpha")}
+
+ +
+
+
+
${Language.get("wcf.style.colorPicker.hexAlpha")}
+
+
+ # + +
+
+
+
+
+ ${Language.get("wcf.style.colorPicker.new")} +
+ +
+
+ +
+ ${Language.get("wcf.style.colorPicker.current")} +
+
+
+ +
+
`, + options: { + onSetup: (content) => { + this.colorInput = content.querySelector("input[type=color]") as HTMLInputElement; + this.colorInput.addEventListener("input", () => this.updateColor()); + this.alphaInput = content.querySelector("input[type=range]") as HTMLInputElement; + this.alphaInput.addEventListener("input", () => this.updateColor()); + + this.newColor = content.querySelector(".colorPickerColorNew > span") as HTMLSpanElement; + this.oldColor = content.querySelector(".colorPickerColorOld > span") as HTMLSpanElement; + + this.colorTextInput = content.querySelector("input[type=text]") as HTMLInputElement; + this.colorTextInput.addEventListener("blur", (ev) => this.updateColorFromHex(ev)); + this.colorTextInput.addEventListener("keypress", (ev) => this.updateColorFromHex(ev)); + + content.querySelector(".formSubmit > .buttonPrimary")!.addEventListener("click", () => this.submitDialog()); + + if (ColorUtil.isValidColor(this.input.value)) { + this.setInitialColor(this.input.value); + } else if (this.element.dataset.color && ColorUtil.isValidColor(this.element.dataset.color)) { + this.setInitialColor(this.element.dataset.color); + } else { + this.setInitialColor("#FFF0"); + } + }, + title: Language.get("wcf.style.colorPicker"), + }, + }; + } + + /** + * Sets the callback called after submitting the dialog. + * + * @deprecated 5.5, only exists for backward compatibility with the old `WCF.ColorPicker`; + * use the constructor options instead + */ + public setCallbackSubmit(callbackSubmit: CallbackSubmit): void { + this.options.callbackSubmit = callbackSubmit; + } + + /** + * Updates the current color after the color or alpha input changes its value. + * + * @since 5.5 + */ + protected updateColor(): void { + this.setColor(this.getColor()); + } + + /** + * Updates the current color after the hex input changes its value. + * + * @since 5.5 + */ + protected updateColorFromHex(event: Event): void { + if (event instanceof KeyboardEvent && event.key !== "Enter") { + return; + } - _marshal(element, options as ColorPickerOptions); + const colorTextInput = this.colorTextInput!; + let color = colorTextInput.value; + + DomUtil.innerError(colorTextInput, null); + if (!ColorUtil.isValidColor(color)) { + if (ColorUtil.isValidColor(`#${color}`)) { + color = `#${color}`; + } else { + DomUtil.innerError(colorTextInput, Language.get("wcf.style.colorPicker.error.invalidColor")); + return; + } + } + + this.setColor(color); + } + + /** + * Returns the current RGBA color set via the color and alpha input. + * + * @since 5.5 + */ + protected getColor(): ColorUtil.RGBA { + const color = this.colorInput!.value; + const alpha = this.alphaInput!.value; + + return { ...(ColorUtil.hexToRgb(color) as ColorUtil.RGB), a: +alpha }; + } + + /** + * Opens the color picker after clicking on the picker button. + * + * @since 5.5 + */ + protected openPicker(): void { + UiDialog.open(this); + } + + /** + * Updates the UI to show the given color. + * + * @since 5.5 + */ + protected setColor(color: ColorUtil.RGBA | string): void { + if (typeof color === "string") { + color = ColorUtil.stringToRgba(color); + } + + this.colorInput!.value = `#${ColorUtil.rgbToHex(color.r, color.g, color.b)}`; + this.alphaInput!.value = color.a.toString(); + + this.newColor!.style.backgroundColor = ColorUtil.rgbaToString(color); + this.colorTextInput!.value = ColorUtil.rgbaToHex(color); + } + + /** + * Updates the UI to show the given color as the initial color. + * + * @since 5.5 + */ + protected setInitialColor(color: ColorUtil.RGBA | string): void { + if (typeof color === "string") { + color = ColorUtil.stringToRgba(color); + } + + this.setColor(color); + + this.oldColor!.style.backgroundColor = ColorUtil.rgbaToString(color); + } + + /** + * Closes the color picker and updates the stored value. + * + * @since 5.5 + */ + protected submitDialog(): void { + const color = this.getColor(); + const colorString = ColorUtil.rgbaToString(color); + + 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; + } + + UiDialog.close(this); + + if (typeof this.options.callbackSubmit === "function") { + this.options.callbackSubmit(color); + } } /** diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Color/Picker.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Color/Picker.js index dddb7b52d5..e2642d3c18 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Color/Picker.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Color/Picker.js @@ -2,53 +2,217 @@ * Wrapper class to provide color picker support. Constructing a new object does not * guarantee the picker to be ready at the time of call. * - * @author Alexander Ebert - * @copyright 2001-2019 WoltLab GmbH + * @author Alexander Ebert, Matthias Schmidt + * @copyright 2001-2021 WoltLab GmbH * @license GNU Lesser General Public License * @module WoltLabSuite/Core/Ui/Color/Picker * @woltlabExcludeBundle all */ -define(["require", "exports", "tslib", "../../Core"], function (require, exports, tslib_1, Core) { +define(["require", "exports", "tslib", "../../Core", "../Dialog", "../../Dom/Util", "../../Language", "../../ColorUtil"], function (require, exports, tslib_1, Core, Dialog_1, Util_1, Language, ColorUtil) { "use strict"; Core = tslib_1.__importStar(Core); - let _marshal = (element, options) => { - if (typeof window.WCF === "object" && typeof window.WCF.ColorPicker === "function") { - _marshal = (element, options) => { - const picker = new window.WCF.ColorPicker(element); - if (typeof options.callbackSubmit === "function") { - picker.setCallbackSubmit(options.callbackSubmit); - } - return picker; - }; - return _marshal(element, options); - } - else { - if (_queue.length === 0) { - window.__wcf_bc_colorPickerInit = () => { - _queue.forEach((data) => { - _marshal(data[0], data[1]); - }); - window.__wcf_bc_colorPickerInit = undefined; - _queue = []; - }; - } - _queue.push([element, options]); - } - }; - let _queue = []; + Dialog_1 = tslib_1.__importDefault(Dialog_1); + Util_1 = tslib_1.__importDefault(Util_1); + Language = tslib_1.__importStar(Language); + ColorUtil = tslib_1.__importStar(ColorUtil); class UiColorPicker { /** - * Initializes a new color picker instance. This is actually just a wrapper that does - * not guarantee the picker to be ready at the time of call. + * Initializes a new color picker instance. */ constructor(element, options) { + this.alphaInput = null; + this.colorInput = null; + this.colorTextInput = null; + this.newColor = null; + this.oldColor = null; if (!(element instanceof Element)) { throw new TypeError("Expected a valid DOM element, use `UiColorPicker.fromSelector()` if you want to use a CSS selector."); } - options = Core.extend({ + this.element = element; + this.input = document.getElementById(element.dataset.store); + if (!this.input) { + throw new Error(`Cannot find input element for color picker ${Util_1.default.identify(element)}.`); + } + this.options = Core.extend({ callbackSubmit: null, }, options || {}); - _marshal(element, options); + element.addEventListener("click", () => this.openPicker()); + } + _dialogSetup() { + return { + id: `${Util_1.default.identify(this.element)}_colorPickerDialog`, + source: ` +
+
+
+
+
${Language.get("wcf.style.colorPicker.color")}
+
+ +
+
+
+
${Language.get("wcf.style.colorPicker.alpha")}
+
+ +
+
+
+
${Language.get("wcf.style.colorPicker.hexAlpha")}
+
+
+ # + +
+
+
+
+
+ ${Language.get("wcf.style.colorPicker.new")} +
+ +
+
+ +
+ ${Language.get("wcf.style.colorPicker.current")} +
+
+
+ +
+
`, + options: { + onSetup: (content) => { + this.colorInput = content.querySelector("input[type=color]"); + this.colorInput.addEventListener("input", () => this.updateColor()); + this.alphaInput = content.querySelector("input[type=range]"); + this.alphaInput.addEventListener("input", () => this.updateColor()); + this.newColor = content.querySelector(".colorPickerColorNew > span"); + this.oldColor = content.querySelector(".colorPickerColorOld > span"); + this.colorTextInput = content.querySelector("input[type=text]"); + this.colorTextInput.addEventListener("blur", (ev) => this.updateColorFromHex(ev)); + this.colorTextInput.addEventListener("keypress", (ev) => this.updateColorFromHex(ev)); + content.querySelector(".formSubmit > .buttonPrimary").addEventListener("click", () => this.submitDialog()); + if (ColorUtil.isValidColor(this.input.value)) { + this.setInitialColor(this.input.value); + } + else if (this.element.dataset.color && ColorUtil.isValidColor(this.element.dataset.color)) { + this.setInitialColor(this.element.dataset.color); + } + else { + this.setInitialColor("#FFF0"); + } + }, + title: Language.get("wcf.style.colorPicker"), + }, + }; + } + /** + * Sets the callback called after submitting the dialog. + * + * @deprecated 5.5, only exists for backward compatibility with the old `WCF.ColorPicker`; + * use the constructor options instead + */ + setCallbackSubmit(callbackSubmit) { + this.options.callbackSubmit = callbackSubmit; + } + /** + * Updates the current color after the color or alpha input changes its value. + * + * @since 5.5 + */ + updateColor() { + this.setColor(this.getColor()); + } + /** + * Updates the current color after the hex input changes its value. + * + * @since 5.5 + */ + updateColorFromHex(event) { + if (event instanceof KeyboardEvent && event.key !== "Enter") { + return; + } + const colorTextInput = this.colorTextInput; + let color = colorTextInput.value; + Util_1.default.innerError(colorTextInput, null); + if (!ColorUtil.isValidColor(color)) { + if (ColorUtil.isValidColor(`#${color}`)) { + color = `#${color}`; + } + else { + Util_1.default.innerError(colorTextInput, Language.get("wcf.style.colorPicker.error.invalidColor")); + return; + } + } + this.setColor(color); + } + /** + * Returns the current RGBA color set via the color and alpha input. + * + * @since 5.5 + */ + getColor() { + const color = this.colorInput.value; + const alpha = this.alphaInput.value; + return Object.assign(Object.assign({}, ColorUtil.hexToRgb(color)), { a: +alpha }); + } + /** + * Opens the color picker after clicking on the picker button. + * + * @since 5.5 + */ + openPicker() { + Dialog_1.default.open(this); + } + /** + * Updates the UI to show the given color. + * + * @since 5.5 + */ + setColor(color) { + if (typeof color === "string") { + color = ColorUtil.stringToRgba(color); + } + this.colorInput.value = `#${ColorUtil.rgbToHex(color.r, color.g, color.b)}`; + this.alphaInput.value = color.a.toString(); + this.newColor.style.backgroundColor = ColorUtil.rgbaToString(color); + this.colorTextInput.value = ColorUtil.rgbaToHex(color); + } + /** + * Updates the UI to show the given color as the initial color. + * + * @since 5.5 + */ + setInitialColor(color) { + if (typeof color === "string") { + color = ColorUtil.stringToRgba(color); + } + this.setColor(color); + this.oldColor.style.backgroundColor = ColorUtil.rgbaToString(color); + } + /** + * Closes the color picker and updates the stored value. + * + * @since 5.5 + */ + submitDialog() { + const color = this.getColor(); + const colorString = ColorUtil.rgbaToString(color); + 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; + } + Dialog_1.default.close(this); + if (typeof this.options.callbackSubmit === "function") { + this.options.callbackSubmit(color); + } } /** * Initializes a color picker for all input elements matching the given selector. diff --git a/wcfsetup/install/files/style/ui/colorPicker.scss b/wcfsetup/install/files/style/ui/colorPicker.scss index d7ed758342..b5b5cde44b 100644 --- a/wcfsetup/install/files/style/ui/colorPicker.scss +++ b/wcfsetup/install/files/style/ui/colorPicker.scss @@ -148,3 +148,45 @@ width: 180px; } } + +/** updated `WoltLabSuite/Core/Ui/Color/Picker` version since 5.5 */ + +.colorPickerColorNew, +.colorPickerColorOld, +.colorPickerButton { + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAABlBMVEX////MzMw46qqDAAAAD0lEQVQI12P4z4Ad4ZAAAH6/D/Hgw85/AAAAAElFTkSuQmCC); + border: 1px solid rgba(0, 0, 0, 1); + box-sizing: content-box; + display: block; + + > span { + display: block; + } +} + +.colorPickerButton { + height: 32px; + width: 50px; + + > span { + height: 32px; + } +} + +.colorPickerComparison { + text-align: center; + + .colorPickerColorNew, + .colorPickerColorOld { + height: 72px; + + > span { + height: 72px; + } + } + + .colorPickerColorOld { + background-position: 8px 0; + border-top-width: 0; + } +} diff --git a/wcfsetup/install/lang/de.xml b/wcfsetup/install/lang/de.xml index 97798c9b0e..438051deef 100644 --- a/wcfsetup/install/lang/de.xml +++ b/wcfsetup/install/lang/de.xml @@ -4511,6 +4511,11 @@ Dateianhänge: + + + + + name}“{/implode}]]> diff --git a/wcfsetup/install/lang/en.xml b/wcfsetup/install/lang/en.xml index 3b672d56f6..85c9cbd22b 100644 --- a/wcfsetup/install/lang/en.xml +++ b/wcfsetup/install/lang/en.xml @@ -4512,6 +4512,11 @@ Attachments: + + + + + name}”{/implode}]]> -- 2.20.1