mirror of
https://github.com/shoelace-style/webawesome.git
synced 2026-01-12 20:19:13 +00:00
Improve color conversions and user input
This commit is contained in:
54
package-lock.json
generated
54
package-lock.json
generated
@@ -129,6 +129,11 @@
|
||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.1.1.tgz",
|
||||
"integrity": "sha512-sLqWxCzC5/QHLhziXSCAksBxHfOnQlhPRVgPK0egEw+ktWvG75T2k+aYWVjVh9+WKeT3tlG3ZNbZQvZLmfuOIw=="
|
||||
},
|
||||
"@sphinxxxx/color-conversion": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@sphinxxxx/color-conversion/-/color-conversion-2.2.2.tgz",
|
||||
"integrity": "sha512-XExJS3cLqgrmNBIP3bBw6+1oQ1ksGjFh0+oClDKFYpCCqx/hlqwWO5KO/S63fzUo67SxI9dMrF0y5T/Ey7h8Zw=="
|
||||
},
|
||||
"@stencil/core": {
|
||||
"version": "1.12.7",
|
||||
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-1.12.7.tgz",
|
||||
@@ -1033,18 +1038,36 @@
|
||||
"object-visit": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"color": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/color/-/color-3.1.2.tgz",
|
||||
"integrity": "sha512-vXTJhHebByxZn3lDvDJYw4lR5+uB3vuoHsuYA5AKuxRVn5wzzIfQKGLBmgdVRHKTJYeK5rvJcHnrd0Li49CFpg==",
|
||||
"requires": {
|
||||
"color-name": "~1.1.4"
|
||||
"color-convert": "^1.9.1",
|
||||
"color-string": "^1.5.2"
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "1.9.3",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
|
||||
"requires": {
|
||||
"color-name": "1.1.3"
|
||||
}
|
||||
},
|
||||
"color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
|
||||
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
|
||||
},
|
||||
"color-string": {
|
||||
"version": "1.5.3",
|
||||
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.3.tgz",
|
||||
"integrity": "sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw==",
|
||||
"requires": {
|
||||
"color-name": "^1.0.0",
|
||||
"simple-swizzle": "^0.2.2"
|
||||
}
|
||||
},
|
||||
"color-support": {
|
||||
"version": "1.1.3",
|
||||
@@ -5395,6 +5418,21 @@
|
||||
"integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==",
|
||||
"dev": true
|
||||
},
|
||||
"simple-swizzle": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
|
||||
"integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=",
|
||||
"requires": {
|
||||
"is-arrayish": "^0.3.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"is-arrayish": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
|
||||
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"slash": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz",
|
||||
|
||||
@@ -41,9 +41,10 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.1.1",
|
||||
"@sphinxxxx/color-conversion": "^2.2.2",
|
||||
"@stencil/core": "^1.12.7",
|
||||
"@stencil/sass": "^1.1.1",
|
||||
"color-convert": "^2.0.1",
|
||||
"color": "^3.1.2",
|
||||
"feather-icons": "^4.28.0",
|
||||
"normalize.css": "^8.0.1",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
|
||||
@@ -102,7 +102,6 @@
|
||||
margin-left: calc(var(--slider-handle-size) / -2);
|
||||
transition: var(--sl-transition-fast) box-shadow, var(--sl-transition-x-fast) top ease,
|
||||
var(--sl-transition-x-fast) left ease;
|
||||
cursor: pointer;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component, Prop, State, h } from '@stencil/core';
|
||||
import convert from 'color-convert';
|
||||
import color from 'color';
|
||||
import { clamp } from '../../utilities/math';
|
||||
|
||||
@Component({
|
||||
@@ -14,6 +14,7 @@ export class ColorPicker {
|
||||
gridHandle: HTMLElement;
|
||||
hueSlider: HTMLElement;
|
||||
hueHandle: HTMLElement;
|
||||
input: HTMLSlInputElement;
|
||||
menu: HTMLElement;
|
||||
trigger: HTMLElement;
|
||||
|
||||
@@ -30,7 +31,6 @@ export class ColorPicker {
|
||||
this.handleHueDrag = this.handleHueDrag.bind(this);
|
||||
this.handleHueKeyDown = this.handleHueKeyDown.bind(this);
|
||||
this.handleUserChange = this.handleUserChange.bind(this);
|
||||
this.handleUserInput = this.handleUserInput.bind(this);
|
||||
}
|
||||
|
||||
@State() hue = 0;
|
||||
@@ -72,43 +72,7 @@ export class ColorPicker {
|
||||
];
|
||||
|
||||
componentWillLoad() {
|
||||
this.syncValue();
|
||||
}
|
||||
|
||||
getHex() {
|
||||
const hsl = [this.hue, this.saturation, this.lightness];
|
||||
const hex = convert.hsl.hex(hsl);
|
||||
const alpha = Math.ceil((this.alpha * 255) / 100 + 0x10000)
|
||||
.toString(16)
|
||||
.substr(-2)
|
||||
.toUpperCase();
|
||||
|
||||
if (this.opacity) {
|
||||
return `#${hex}${alpha}`;
|
||||
} else {
|
||||
return `#${hex}`;
|
||||
}
|
||||
}
|
||||
|
||||
getRGB() {
|
||||
const hsl = [this.hue, this.saturation, this.lightness];
|
||||
const rgb = convert.hsl.rgb(hsl);
|
||||
|
||||
if (this.opacity) {
|
||||
return `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${this.alpha}%)`;
|
||||
} else {
|
||||
return `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`;
|
||||
}
|
||||
}
|
||||
|
||||
getHSL() {
|
||||
const hsl = [this.hue, this.saturation, this.lightness];
|
||||
|
||||
if (this.opacity) {
|
||||
return `hsla(${hsl[0]}, ${hsl[1]}%, ${hsl[2]}%, ${this.alpha}%)`;
|
||||
} else {
|
||||
return `hsl(${hsl[0]}, ${hsl[1]}%, ${hsl[2]}%)`;
|
||||
}
|
||||
this.setColor(`hsla(${this.hue}, ${this.saturation}%, ${this.lightness}%, ${this.alpha}%)`);
|
||||
}
|
||||
|
||||
handleHueInput(event: Event) {
|
||||
@@ -140,7 +104,7 @@ export class ColorPicker {
|
||||
|
||||
this.handleDrag(event, container, x => {
|
||||
this.alpha = clamp(Math.round((x / width) * 100), 0, 100);
|
||||
this.syncValue();
|
||||
this.syncInputValue();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -153,7 +117,7 @@ export class ColorPicker {
|
||||
|
||||
this.handleDrag(event, container, x => {
|
||||
this.hue = clamp(Math.round((x / width) * 360), 0, 360);
|
||||
this.syncValue();
|
||||
this.syncInputValue();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -167,7 +131,7 @@ export class ColorPicker {
|
||||
this.handleDrag(event, container, (x, y) => {
|
||||
this.saturation = clamp(Math.round((x / width) * 100), 0, 100);
|
||||
this.lightness = clamp(Math.round(100 - (y / height) * 100), 0, 100);
|
||||
this.syncValue();
|
||||
this.syncInputValue();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -270,112 +234,124 @@ export class ColorPicker {
|
||||
handleUserChange(event: CustomEvent) {
|
||||
const target = event.target as HTMLInputElement;
|
||||
|
||||
if (!this.setColor(target.value)) {
|
||||
// Revert to the last valid color
|
||||
this.setColor(this.value);
|
||||
}
|
||||
this.setColor(target.value);
|
||||
|
||||
target.value = this.value;
|
||||
}
|
||||
|
||||
handleUserInput(event: KeyboardEvent) {
|
||||
const target = event.target as HTMLInputElement;
|
||||
this.setColor(target.value, false);
|
||||
}
|
||||
normalizeColorString(colorString: string) {
|
||||
//
|
||||
// The color module we're using doesn't parse % values for the alpha channel in RGBA and HSLA. It also doesn't parse
|
||||
// hex colors when the # is missing. This pre-parser tries to normalize these edge cases to provide a better
|
||||
// experience for users who type in color values.
|
||||
//
|
||||
if (/rgba?/.test(colorString)) {
|
||||
const rgba = colorString
|
||||
.replace(/[^\d.%]/g, ' ')
|
||||
.split(' ')
|
||||
.map(val => val.trim())
|
||||
.filter(val => val.length);
|
||||
|
||||
parseColor(color: string) {
|
||||
const hexPattern = /#?([a-f0-9]{1,2})([a-f0-9]{1,2})([a-f0-9]{1,2})([a-f0-9]{1,2})?/i;
|
||||
let hue = 0;
|
||||
let saturation = 0;
|
||||
let lightness = 0;
|
||||
let alpha = 100;
|
||||
|
||||
color = color.trim().toLowerCase();
|
||||
|
||||
if (/^rgba?/i.test(color)) {
|
||||
// Parse as RGB
|
||||
const rgb = color.replace(/[^\d,.%]/g, '').split(',');
|
||||
if (parseInt(rgb[0]) < 0 || parseInt(rgb[0]) > 255) return false;
|
||||
if (parseInt(rgb[1]) < 0 || parseInt(rgb[1]) > 255) return false;
|
||||
if (parseInt(rgb[2]) < 0 || parseInt(rgb[2]) > 255) return false;
|
||||
|
||||
[hue, saturation, lightness] = convert.rgb.hsl(rgb);
|
||||
|
||||
if (rgb[3] && rgb[3].indexOf('%') > -1) {
|
||||
alpha = Number(rgb[3].replace('%', ''));
|
||||
} else if (rgb[3]) {
|
||||
alpha = Number(rgb[3]) * 100;
|
||||
if (rgba[3] && rgba[3].indexOf('%') > -1) {
|
||||
rgba[3] = (Number(rgba[3].replace(/%/g, '')) / 100).toString();
|
||||
}
|
||||
} else if (/^hsla?/i.test(color)) {
|
||||
// Parse as HSL
|
||||
const hsl = color.replace(/[^\d,.%]/g, '').split(',');
|
||||
if (parseInt(hsl[0]) < 0 || parseInt(hsl[0]) > 360) return false;
|
||||
if (parseInt(hsl[1]) < 0 || parseInt(hsl[1]) > 100) return false;
|
||||
if (parseInt(hsl[2]) < 0 || parseInt(hsl[2]) > 100) return false;
|
||||
|
||||
hue = Number(hsl[0]);
|
||||
saturation = Number(hsl[0]);
|
||||
lightness = Number(hsl[0]);
|
||||
|
||||
if (hsl[3] && hsl[3].indexOf('%') > -1) {
|
||||
alpha = Number(hsl[3].replace('%', ''));
|
||||
} else if (hsl[3]) {
|
||||
alpha = Number(hsl[3]) * 100;
|
||||
}
|
||||
} else if (hexPattern.test(color)) {
|
||||
// Parse as hex
|
||||
const hex = color.match(hexPattern).slice(1, 5);
|
||||
if (!/^[a-f0-9]{1,2}$/i.test(hex[0])) return false;
|
||||
if (!/^[a-f0-9]{1,2}$/i.test(hex[1])) return false;
|
||||
if (!/^[a-f0-9]{1,2}$/i.test(hex[2])) return false;
|
||||
|
||||
[hue, saturation, lightness] = convert.hex.hsl(hex.join(''));
|
||||
|
||||
if (hex[3]) {
|
||||
alpha = Math.round((parseInt(hex[3], 16) / 255) * 100);
|
||||
}
|
||||
} else if (/[a-z]+/.test(color)) {
|
||||
// Parse as CSS color
|
||||
try {
|
||||
[hue, saturation, lightness] = convert.keyword.hsl(color);
|
||||
alpha = 100;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return `rgba(${rgba[0]}, ${rgba[1]}, ${rgba[2]}, ${rgba[3]})`;
|
||||
}
|
||||
|
||||
return {
|
||||
hue,
|
||||
saturation,
|
||||
lightness,
|
||||
alpha
|
||||
};
|
||||
if (/hsla?/.test(colorString)) {
|
||||
const hsla = colorString
|
||||
.replace(/[^\d.%]/g, ' ')
|
||||
.split(' ')
|
||||
.map(val => val.trim())
|
||||
.filter(val => val.length);
|
||||
|
||||
if (hsla[3] && hsla[3].indexOf('%') > -1) {
|
||||
hsla[3] = (Number(hsla[3].replace(/%/g, '')) / 100).toString();
|
||||
}
|
||||
|
||||
return `hsla(${hsla[0]}, ${hsla[1]}, ${hsla[2]}, ${hsla[3]})`;
|
||||
}
|
||||
|
||||
if (/^[0-9a-f]+$/.test(colorString)) {
|
||||
return `#${colorString}`;
|
||||
}
|
||||
|
||||
return colorString;
|
||||
}
|
||||
|
||||
setColor(color: string, syncValue = true) {
|
||||
const parsed = this.parseColor(color);
|
||||
parseColor(colorString: string) {
|
||||
function toHex(value: number) {
|
||||
const hex = Math.round(value).toString(16);
|
||||
return hex.length == 1 ? `0${hex}` : hex;
|
||||
}
|
||||
|
||||
if (!parsed) {
|
||||
let parsed: any;
|
||||
|
||||
// NOTE: The color module doesn't support % values for alpha channels, so we need to normalize them to 0-1 decimals
|
||||
colorString = this.normalizeColorString(colorString);
|
||||
|
||||
try {
|
||||
parsed = color(colorString);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.hue = parsed.hue;
|
||||
this.saturation = parsed.saturation;
|
||||
this.lightness = parsed.lightness;
|
||||
this.alpha = this.opacity ? parsed.alpha : 100;
|
||||
const h = parsed.hsl().color[0];
|
||||
const s = parsed.hsl().color[1];
|
||||
const l = parsed.hsl().color[2];
|
||||
const a = parsed.hsl().valpha;
|
||||
|
||||
if (syncValue) {
|
||||
this.syncValue();
|
||||
const r = parsed.rgb().color[0];
|
||||
const g = parsed.rgb().color[0];
|
||||
const b = parsed.rgb().color[0];
|
||||
|
||||
const hex = `#${toHex(parsed.rgb().color[0])}${toHex(parsed.rgb().color[1])}${toHex(
|
||||
parsed.rgb().color[2]
|
||||
)}`.toUpperCase();
|
||||
const hexa = hex + toHex((parsed.rgb().valpha || 1) * 255).toUpperCase();
|
||||
|
||||
return {
|
||||
hsl: { h, s, l, string: `hsl(${h}, ${s}%, ${l}%)` },
|
||||
hsla: { h, s, l, a, string: `hsla(${h}, ${s}%, ${l}%, ${a})` },
|
||||
rgb: { r, g, b, string: `rgb(${r}, ${g}, ${b})` },
|
||||
rgba: { r, g, b, a, string: `rgba(${r}, ${g}, ${b}, ${a})` },
|
||||
hex,
|
||||
hexa
|
||||
};
|
||||
}
|
||||
|
||||
setColor(colorString: string) {
|
||||
const newColor = this.parseColor(colorString);
|
||||
|
||||
if (!newColor) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.hue = newColor.hsla.h;
|
||||
this.saturation = newColor.hsla.s;
|
||||
this.lightness = newColor.hsla.l;
|
||||
this.alpha = this.opacity ? newColor.hsla.a * 100 : 100;
|
||||
|
||||
this.syncInputValue();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
syncValue() {
|
||||
syncInputValue() {
|
||||
const currentColor = this.parseColor(`hsla(${this.hue}, ${this.saturation}%, ${this.lightness}%, ${this.alpha}%)`);
|
||||
|
||||
if (!currentColor) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Update the value
|
||||
if (this.format === 'hsl') {
|
||||
this.value = this.getHSL();
|
||||
this.value = this.opacity ? currentColor.hsla.string : currentColor.hsl.string;
|
||||
} else if (this.format === 'rgb') {
|
||||
this.value = this.getRGB();
|
||||
this.value = this.opacity ? currentColor.rgba.string : currentColor.rgb.string;
|
||||
} else {
|
||||
this.value = this.getHex();
|
||||
this.value = this.opacity ? currentColor.hexa : currentColor.hex;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -403,6 +379,7 @@ export class ColorPicker {
|
||||
left: `${x}%`
|
||||
}}
|
||||
role="slider"
|
||||
aria-label="HSL"
|
||||
aria-valuetext={`hsl(${this.hue}, ${this.saturation}%, ${this.lightness}%)`}
|
||||
tabIndex={0}
|
||||
onKeyDown={this.handleGridKeyDown}
|
||||
@@ -424,6 +401,7 @@ export class ColorPicker {
|
||||
left: `${this.hue === 0 ? 0 : 100 / (360 / this.hue)}%`
|
||||
}}
|
||||
role="slider"
|
||||
aria-label="hue"
|
||||
aria-orientation="horizontal"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="360"
|
||||
@@ -457,6 +435,7 @@ export class ColorPicker {
|
||||
left: `${this.alpha}%`
|
||||
}}
|
||||
role="slider"
|
||||
aria-label="alpha"
|
||||
aria-orientation="horizontal"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
@@ -479,11 +458,11 @@ export class ColorPicker {
|
||||
<div class="sl-color-picker__inputs">
|
||||
<div class="sl-color-picker__input">
|
||||
<sl-input
|
||||
ref={el => (this.input = el)}
|
||||
size="small"
|
||||
type="text"
|
||||
pattern="[a-fA-F\d]+"
|
||||
value={this.value}
|
||||
onInput={this.handleUserInput}
|
||||
onSlChange={this.handleUserChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Color Picker
|
||||
|
||||
```html preview
|
||||
<sl-color-picker format="hex"></sl-color-picker>
|
||||
<sl-color-picker opacity format="hex"></sl-color-picker>
|
||||
```
|
||||
|
||||
|
||||
Reference in New Issue
Block a user