From: Alexander Ebert Date: Mon, 30 May 2022 14:54:12 +0000 (+0200) Subject: Overhauled color picker with RGBA and HSL X-Git-Tag: 5.5.0_Beta_4~7^2~4 X-Git-Url: https://git.stricted.de/?a=commitdiff_plain;h=520129171b564493b6e63a3536106817e7af0608;p=GitHub%2FWoltLab%2FWCF.git Overhauled color picker with RGBA and HSL --- diff --git a/ts/WoltLabSuite/Core/Ui/Color/Picker.ts b/ts/WoltLabSuite/Core/Ui/Color/Picker.ts index cb564b5c69..b217807f0e 100644 --- a/ts/WoltLabSuite/Core/Ui/Color/Picker.ts +++ b/ts/WoltLabSuite/Core/Ui/Color/Picker.ts @@ -19,9 +19,23 @@ import * as ColorUtil from "../../ColorUtil"; type CallbackSubmit = (data: ColorUtil.RGBA) => void; const enum Channel { - R, - G, - B, + R = "r", + G = "g", + B = "b", + A = "a", +} + +const enum HSL { + Hue = "hue", + Saturation = "saturation", + Lightness = "lightness", +} + +const enum ColorSource { + HEX = "hex", + HSL = "hsl", + RGBA = "rgba", + Setup = "setup", } interface ColorPickerOptions { @@ -29,14 +43,15 @@ interface ColorPickerOptions { } class UiColorPicker implements DialogCallbackObject { - protected alphaInput: HTMLInputElement | null = null; private readonly channels = new Map(); protected colorInput: HTMLInputElement | null = null; protected colorTextInput: HTMLInputElement | null = null; protected readonly element: HTMLElement; + private readonly hsl = new Map(); + private hslContainer?: HTMLElement = undefined; protected readonly input: HTMLInputElement; - protected newColor: HTMLSpanElement | null = null; - protected oldColor: HTMLSpanElement | null = null; + protected newColor?: HTMLElement = undefined; + protected oldColor?: HTMLElement = undefined; protected readonly options: ColorPickerOptions; /** @@ -70,29 +85,38 @@ class UiColorPicker implements DialogCallbackObject { id: `${DomUtil.identify(this.element)}_colorPickerDialog`, source: `
-
-
-
-
${Language.get("wcf.style.colorPicker.color")}
+
+
+
${Language.get("wcf.style.colorPicker.hue")}
-
- R - -
-
- G - -
-
- B - -
+
-
-
-
${Language.get("wcf.style.colorPicker.alpha")}
+
+
+
${Language.get("wcf.style.colorPicker.saturation")}
- + +
+
+
+
${Language.get("wcf.style.colorPicker.lightness")}
+
+ +
+
+
+
+
+
+
${Language.get("wcf.style.colorPicker.color")}
+
+ rgba( + + + + / + + )
@@ -105,7 +129,7 @@ class UiColorPicker implements DialogCallbackObject {
-
+
${Language.get("wcf.style.colorPicker.new")}
@@ -117,7 +141,7 @@ class UiColorPicker implements DialogCallbackObject {
- +
`, options: { @@ -125,15 +149,24 @@ class UiColorPicker implements DialogCallbackObject { this.channels.set(Channel.R, content.querySelector('input[data-channel="r"]') as HTMLInputElement); this.channels.set(Channel.G, content.querySelector('input[data-channel="g"]') as HTMLInputElement); this.channels.set(Channel.B, content.querySelector('input[data-channel="b"]') as HTMLInputElement); + this.channels.set(Channel.A, content.querySelector('input[data-channel="a"]') as HTMLInputElement); this.channels.forEach((input) => { - input.addEventListener("input", () => this.updateColor()); + input.addEventListener("input", () => this.updateColor(ColorSource.RGBA)); }); - this.alphaInput = content.querySelector("input[type=range]") as HTMLInputElement; - this.alphaInput.addEventListener("input", () => this.updateColor()); + this.hslContainer = content.querySelector(".colorPickerHsvContainer") as HTMLElement; + this.hsl.set(HSL.Hue, content.querySelector('input[data-coordinate="hue"]') as HTMLInputElement); + this.hsl.set( + HSL.Saturation, + content.querySelector('input[data-coordinate="saturation"]') as HTMLInputElement, + ); + this.hsl.set(HSL.Lightness, content.querySelector('input[data-coordinate="lightness"]') as HTMLInputElement); + this.hsl.forEach((input) => { + input.addEventListener("input", () => this.updateColor(ColorSource.HSL)); + }); - this.newColor = content.querySelector(".colorPickerColorNew > span") as HTMLSpanElement; - this.oldColor = content.querySelector(".colorPickerColorOld > span") as HTMLSpanElement; + this.newColor = content.querySelector(".colorPickerColorNew > span") as HTMLElement; + this.oldColor = content.querySelector(".colorPickerColorOld > span") as HTMLElement; this.colorTextInput = content.querySelector("input[type=text]") as HTMLInputElement; this.colorTextInput.addEventListener("blur", (ev) => this.updateColorFromHex(ev)); @@ -169,8 +202,8 @@ class UiColorPicker implements DialogCallbackObject { * * @since 5.5 */ - protected updateColor(): void { - this.setColor(this.getColor()); + protected updateColor(source: ColorSource): void { + this.setColor(this.getColor(source), source); } /** @@ -196,7 +229,7 @@ class UiColorPicker implements DialogCallbackObject { } } - this.setColor(color); + this.setColor(color, ColorSource.HEX); } /** @@ -204,12 +237,27 @@ class UiColorPicker implements DialogCallbackObject { * * @since 5.5 */ - protected getColor(): ColorUtil.RGBA { + protected getColor(source: ColorSource): ColorUtil.RGBA { + const a = parseFloat(this.channels.get(Channel.A)!.value); + + if (source === ColorSource.HSL) { + const rgb = ColorUtil.hslToRgb( + parseInt(this.hsl.get(HSL.Hue)!.value, 10), + parseInt(this.hsl.get(HSL.Saturation)!.value, 10), + parseInt(this.hsl.get(HSL.Lightness)!.value, 10), + ); + + return { + ...rgb, + a, + }; + } + return { r: parseInt(this.channels.get(Channel.R)!.value, 10), g: parseInt(this.channels.get(Channel.G)!.value, 10), b: parseInt(this.channels.get(Channel.B)!.value, 10), - a: parseInt(this.alphaInput!.value, 10), + a, }; } @@ -227,18 +275,36 @@ class UiColorPicker implements DialogCallbackObject { * * @since 5.5 */ - protected setColor(color: ColorUtil.RGBA | string): void { + protected setColor(color: ColorUtil.RGBA | string, source: ColorSource): void { if (typeof color === "string") { color = ColorUtil.stringToRgba(color); } - this.channels.get(Channel.R)!.value = color.r.toString(); - this.channels.get(Channel.G)!.value = color.g.toString(); - this.channels.get(Channel.B)!.value = color.b.toString(); - this.alphaInput!.value = color.a.toString(); + const { r, g, b, a } = color; + const { h, s, l } = ColorUtil.rgbToHsl(r, g, b); + + if (source !== ColorSource.HSL) { + this.hsl.get(HSL.Hue)!.value = h.toString(); + this.hsl.get(HSL.Saturation)!.value = s.toString(); + this.hsl.get(HSL.Lightness)!.value = l.toString(); + } + + this.hslContainer!.style.setProperty(`--${HSL.Hue}`, `${h}`); + this.hslContainer!.style.setProperty(`--${HSL.Saturation}`, `${s}%`); + this.hslContainer!.style.setProperty(`--${HSL.Lightness}`, `${l}%`); + + if (source !== ColorSource.RGBA) { + this.channels.get(Channel.R)!.value = r.toString(); + this.channels.get(Channel.G)!.value = g.toString(); + this.channels.get(Channel.B)!.value = b.toString(); + this.channels.get(Channel.A)!.value = a.toString(); + } this.newColor!.style.backgroundColor = ColorUtil.rgbaToString(color); - this.colorTextInput!.value = ColorUtil.rgbaToHex(color); + + if (source !== ColorSource.HEX) { + this.colorTextInput!.value = ColorUtil.rgbaToHex(color); + } } /** @@ -251,7 +317,7 @@ class UiColorPicker implements DialogCallbackObject { color = ColorUtil.stringToRgba(color); } - this.setColor(color); + this.setColor(color, ColorSource.Setup); this.oldColor!.style.backgroundColor = ColorUtil.rgbaToString(color); } @@ -262,7 +328,7 @@ class UiColorPicker implements DialogCallbackObject { * @since 5.5 */ protected submitDialog(): void { - const color = this.getColor(); + const color = this.getColor(ColorSource.RGBA); const colorString = ColorUtil.rgbaToString(color); this.oldColor!.style.backgroundColor = colorString; 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 9fcd975eee..1832dc180e 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Color/Picker.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Color/Picker.js @@ -20,12 +20,13 @@ define(["require", "exports", "tslib", "../../Core", "../Dialog", "../../Dom/Uti * Initializes a new color picker instance. */ constructor(element, options) { - this.alphaInput = null; this.channels = new Map(); this.colorInput = null; this.colorTextInput = null; - this.newColor = null; - this.oldColor = null; + this.hsl = new Map(); + this.hslContainer = undefined; + this.newColor = undefined; + this.oldColor = undefined; if (!(element instanceof Element)) { throw new TypeError("Expected a valid DOM element, use `UiColorPicker.fromSelector()` if you want to use a CSS selector."); } @@ -44,29 +45,38 @@ define(["require", "exports", "tslib", "../../Core", "../Dialog", "../../Dom/Uti id: `${Util_1.default.identify(this.element)}_colorPickerDialog`, source: `
-
-
-
-
${Language.get("wcf.style.colorPicker.color")}
+
+
+
${Language.get("wcf.style.colorPicker.hue")}
-
- R - -
-
- G - -
-
- B - -
+
-
-
-
${Language.get("wcf.style.colorPicker.alpha")}
+
+
+
${Language.get("wcf.style.colorPicker.saturation")}
+
+ +
+
+
+
${Language.get("wcf.style.colorPicker.lightness")}
- + +
+
+
+
+
+
+
${Language.get("wcf.style.colorPicker.color")}
+
+ rgba( + + + + / + + )
@@ -79,7 +89,7 @@ define(["require", "exports", "tslib", "../../Core", "../Dialog", "../../Dom/Uti
-
+
${Language.get("wcf.style.colorPicker.new")}
@@ -91,19 +101,25 @@ define(["require", "exports", "tslib", "../../Core", "../Dialog", "../../Dom/Uti
- +
`, options: { onSetup: (content) => { - this.channels.set(0 /* R */, content.querySelector('input[data-channel="r"]')); - this.channels.set(1 /* G */, content.querySelector('input[data-channel="g"]')); - this.channels.set(2 /* B */, content.querySelector('input[data-channel="b"]')); + this.channels.set("r" /* R */, content.querySelector('input[data-channel="r"]')); + this.channels.set("g" /* G */, content.querySelector('input[data-channel="g"]')); + this.channels.set("b" /* B */, content.querySelector('input[data-channel="b"]')); + this.channels.set("a" /* A */, content.querySelector('input[data-channel="a"]')); this.channels.forEach((input) => { - input.addEventListener("input", () => this.updateColor()); + input.addEventListener("input", () => this.updateColor("rgba" /* RGBA */)); + }); + this.hslContainer = content.querySelector(".colorPickerHsvContainer"); + this.hsl.set("hue" /* Hue */, content.querySelector('input[data-coordinate="hue"]')); + this.hsl.set("saturation" /* Saturation */, content.querySelector('input[data-coordinate="saturation"]')); + this.hsl.set("lightness" /* Lightness */, content.querySelector('input[data-coordinate="lightness"]')); + this.hsl.forEach((input) => { + input.addEventListener("input", () => this.updateColor("hsl" /* HSL */)); }); - 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]"); @@ -138,8 +154,8 @@ define(["require", "exports", "tslib", "../../Core", "../Dialog", "../../Dom/Uti * * @since 5.5 */ - updateColor() { - this.setColor(this.getColor()); + updateColor(source) { + this.setColor(this.getColor(source), source); } /** * Updates the current color after the hex input changes its value. @@ -162,19 +178,24 @@ define(["require", "exports", "tslib", "../../Core", "../Dialog", "../../Dom/Uti return; } } - this.setColor(color); + this.setColor(color, "hex" /* HEX */); } /** * Returns the current RGBA color set via the color and alpha input. * * @since 5.5 */ - getColor() { + getColor(source) { + const a = parseFloat(this.channels.get("a" /* A */).value); + if (source === "hsl" /* HSL */) { + const rgb = ColorUtil.hslToRgb(parseInt(this.hsl.get("hue" /* Hue */).value, 10), parseInt(this.hsl.get("saturation" /* Saturation */).value, 10), parseInt(this.hsl.get("lightness" /* Lightness */).value, 10)); + return Object.assign(Object.assign({}, rgb), { a }); + } return { - r: parseInt(this.channels.get(0 /* R */).value, 10), - g: parseInt(this.channels.get(1 /* G */).value, 10), - b: parseInt(this.channels.get(2 /* B */).value, 10), - a: parseInt(this.alphaInput.value, 10), + r: parseInt(this.channels.get("r" /* R */).value, 10), + g: parseInt(this.channels.get("g" /* G */).value, 10), + b: parseInt(this.channels.get("b" /* B */).value, 10), + a, }; } /** @@ -190,16 +211,30 @@ define(["require", "exports", "tslib", "../../Core", "../Dialog", "../../Dom/Uti * * @since 5.5 */ - setColor(color) { + setColor(color, source) { if (typeof color === "string") { color = ColorUtil.stringToRgba(color); } - this.channels.get(0 /* R */).value = color.r.toString(); - this.channels.get(1 /* G */).value = color.g.toString(); - this.channels.get(2 /* B */).value = color.b.toString(); - this.alphaInput.value = color.a.toString(); + const { r, g, b, a } = color; + const { h, s, l } = ColorUtil.rgbToHsl(r, g, b); + if (source !== "hsl" /* HSL */) { + this.hsl.get("hue" /* Hue */).value = h.toString(); + this.hsl.get("saturation" /* Saturation */).value = s.toString(); + this.hsl.get("lightness" /* Lightness */).value = l.toString(); + } + this.hslContainer.style.setProperty(`--${"hue" /* Hue */}`, `${h}`); + this.hslContainer.style.setProperty(`--${"saturation" /* Saturation */}`, `${s}%`); + this.hslContainer.style.setProperty(`--${"lightness" /* Lightness */}`, `${l}%`); + if (source !== "rgba" /* RGBA */) { + this.channels.get("r" /* R */).value = r.toString(); + this.channels.get("g" /* G */).value = g.toString(); + this.channels.get("b" /* B */).value = b.toString(); + this.channels.get("a" /* A */).value = a.toString(); + } this.newColor.style.backgroundColor = ColorUtil.rgbaToString(color); - this.colorTextInput.value = ColorUtil.rgbaToHex(color); + if (source !== "hex" /* HEX */) { + this.colorTextInput.value = ColorUtil.rgbaToHex(color); + } } /** * Updates the UI to show the given color as the initial color. @@ -210,7 +245,7 @@ define(["require", "exports", "tslib", "../../Core", "../Dialog", "../../Dom/Uti if (typeof color === "string") { color = ColorUtil.stringToRgba(color); } - this.setColor(color); + this.setColor(color, "setup" /* Setup */); this.oldColor.style.backgroundColor = ColorUtil.rgbaToString(color); } /** @@ -219,7 +254,7 @@ define(["require", "exports", "tslib", "../../Core", "../Dialog", "../../Dom/Uti * @since 5.5 */ submitDialog() { - const color = this.getColor(); + const color = this.getColor("rgba" /* RGBA */); const colorString = ColorUtil.rgbaToString(color); this.oldColor.style.backgroundColor = colorString; this.input.value = colorString; diff --git a/wcfsetup/install/files/style/ui/colorPicker.scss b/wcfsetup/install/files/style/ui/colorPicker.scss index fde7af87a7..f0e305bba7 100644 --- a/wcfsetup/install/files/style/ui/colorPicker.scss +++ b/wcfsetup/install/files/style/ui/colorPicker.scss @@ -174,23 +174,143 @@ } .colorPickerComparison { + --border-radius: 5px; + + display: grid; + grid-template-rows: min-content auto auto min-content; text-align: center; +} - .colorPickerColorNew, - .colorPickerColorOld { - height: 72px; +.colorPickerColorNew { + border-radius: var(--border-radius) var(--border-radius) 0 0; +} - > span { - height: 72px; - } +.colorPickerColorOld { + border-radius: 0 0 var(--border-radius) var(--border-radius); +} + +.colorPickerChannels { + align-items: center; + column-gap: 3px; + color: $wcfContentDimmedText; + display: flex !important; + + input[type="number"] { + padding: 4px; + text-align: center; } +} - .colorPickerColorOld { - background-position: 8px 0; - border-top-width: 0; +.colorPickerColorNew, +.colorPickerColorOld { + overflow: hidden; + + > span { + height: 100%; } } +.colorPickerColorOld { + background-position: 8px 0; + border-top-width: 0; +} + .colorPickerChannel { display: inline-flex; } + +.colorPickerHslRange, +.colorPickerHslRange::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; +} + +.colorPickerHslRange { + width: 100%; + + //&::-moz-range-track, + &::-webkit-slider-runnable-track { + background-image: var(--track-image); + height: 10px; + border-radius: 5px; + } + + //&::-moz-range-thumb, + &::-webkit-slider-thumb { + background-color: hsl(var(--hue), var(--saturation), var(--lightness)); + border: 4px solid #fff; + border-radius: 50%; + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.025), 0 1px 5px rgba(0, 0, 0, 0.25); + cursor: pointer; + height: 24px; + margin-top: -6px; + width: 24px; + } +} + +.colorPickerHslRange[data-coordinate="hue"] { + --track-image: linear-gradient( + to right, + hsl(0, var(--saturation), var(--lightness)), + hsl(10, var(--saturation), var(--lightness)), + hsl(20, var(--saturation), var(--lightness)), + hsl(30, var(--saturation), var(--lightness)), + hsl(40, var(--saturation), var(--lightness)), + hsl(50, var(--saturation), var(--lightness)), + hsl(60, var(--saturation), var(--lightness)), + hsl(70, var(--saturation), var(--lightness)), + hsl(80, var(--saturation), var(--lightness)), + hsl(90, var(--saturation), var(--lightness)), + hsl(100, var(--saturation), var(--lightness)), + hsl(110, var(--saturation), var(--lightness)), + hsl(120, var(--saturation), var(--lightness)), + hsl(130, var(--saturation), var(--lightness)), + hsl(140, var(--saturation), var(--lightness)), + hsl(150, var(--saturation), var(--lightness)), + hsl(160, var(--saturation), var(--lightness)), + hsl(170, var(--saturation), var(--lightness)), + hsl(180, var(--saturation), var(--lightness)), + hsl(190, var(--saturation), var(--lightness)), + hsl(200, var(--saturation), var(--lightness)), + hsl(210, var(--saturation), var(--lightness)), + hsl(220, var(--saturation), var(--lightness)), + hsl(230, var(--saturation), var(--lightness)), + hsl(240, var(--saturation), var(--lightness)), + hsl(250, var(--saturation), var(--lightness)), + hsl(260, var(--saturation), var(--lightness)), + hsl(270, var(--saturation), var(--lightness)), + hsl(280, var(--saturation), var(--lightness)), + hsl(290, var(--saturation), var(--lightness)), + hsl(300, var(--saturation), var(--lightness)), + hsl(310, var(--saturation), var(--lightness)), + hsl(320, var(--saturation), var(--lightness)), + hsl(330, var(--saturation), var(--lightness)), + hsl(340, var(--saturation), var(--lightness)), + hsl(350, var(--saturation), var(--lightness)), + hsl(359, var(--saturation), var(--lightness)) + ); +} + +.colorPickerHslRange[data-coordinate="saturation"] { + --track-image: linear-gradient( + to right, + hsl(var(--hue), 0%, var(--lightness)) 0%, + hsl(var(--hue), 100%, var(--lightness)) 100% + ); +} + +.colorPickerHslRange[data-coordinate="lightness"] { + --track-image: linear-gradient( + to right, + hsl(var(--hue), var(--saturation), 0%) 0%, + hsl(var(--hue), var(--saturation), 50%) 50%, + hsl(var(--hue), var(--saturation), 100%) 100% + ); +} + +.colorPickerValueContainer { + column-gap: 20px; + display: grid; + grid-template-columns: min-content auto; + margin-top: 20px; +} diff --git a/wcfsetup/install/lang/de.xml b/wcfsetup/install/lang/de.xml index 5d7bbf9f62..4971a7e4ca 100644 --- a/wcfsetup/install/lang/de.xml +++ b/wcfsetup/install/lang/de.xml @@ -4533,11 +4533,13 @@ Dateianhänge: - + + + name}“{/implode}]]> @@ -5622,5 +5624,6 @@ Benachrichtigungen auf {PAGE_TITLE|phra + diff --git a/wcfsetup/install/lang/en.xml b/wcfsetup/install/lang/en.xml index 4c82fd7071..12ebd25d58 100644 --- a/wcfsetup/install/lang/en.xml +++ b/wcfsetup/install/lang/en.xml @@ -4535,11 +4535,13 @@ Attachments: - + + + name}”{/implode}]]> @@ -5624,5 +5626,6 @@ your notifications on {PAGE_TITLE|phras +