From 9178be576b57420b094afc77f4bddbb90dd8edd2 Mon Sep 17 00:00:00 2001 From: Alan Chambers Date: Tue, 13 Dec 2022 16:01:07 +0000 Subject: [PATCH] color-picker library change and hsv format support (#1072) * sl-color-picker use lib '@ctrl/tinycolor' instead of 'color' typescript, esm and smaller * parseColor adjustments removed normalizeColorString and other tweaks * added hsv format * fixed const --- docs/components/color-picker.md | 6 +- package-lock.json | 139 +++----------------- package.json | 3 +- src/components/color-picker/color-picker.ts | 137 +++++++------------ 4 files changed, 73 insertions(+), 212 deletions(-) diff --git a/docs/components/color-picker.md b/docs/components/color-picker.md index 92c6b2b5..1fd0487a 100644 --- a/docs/components/color-picker.md +++ b/docs/components/color-picker.md @@ -32,7 +32,7 @@ const App = () => ; ### Opacity -Use the `opacity` attribute to enable the opacity slider. When this is enabled, the value will be displayed as HEXA, RGBA, or HSLA based on `format`. +Use the `opacity` attribute to enable the opacity slider. When this is enabled, the value will be displayed as HEXA, RGBA, HSLA or HSVA based on `format`. ```html preview @@ -46,7 +46,7 @@ const App = () => ; ### Formats -Set the color picker's format with the `format` attribute. Valid options include `hex`, `rgb`, and `hsl`. Note that the color picker's input will accept any parsable format (including CSS color names) regardless of this option. +Set the color picker's format with the `format` attribute. Valid options include `hex`, `rgb`, `hsl` and `hsv`. Note that the color picker's input will accept any parsable format (including CSS color names) regardless of this option. To prevent users from toggling the format themselves, add the `no-format-toggle` attribute. @@ -54,6 +54,7 @@ To prevent users from toggling the format themselves, add the `no-format-toggle` + ``` ```jsx react @@ -64,6 +65,7 @@ const App = () => ( + ); ``` diff --git a/package-lock.json b/package-lock.json index 37f58d4f..a7f389a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,18 +9,17 @@ "version": "2.0.0-beta.86", "license": "MIT", "dependencies": { + "@ctrl/tinycolor": "^3.5.0", "@floating-ui/dom": "^1.0.7", "@lit-labs/react": "^1.1.0", "@shoelace-style/animations": "^1.1.0", "@shoelace-style/localize": "^3.0.3", - "color": "4.2", "lit": "^2.4.1", "qr-creator": "^1.0.0" }, "devDependencies": { "@custom-elements-manifest/analyzer": "^0.6.6", "@open-wc/testing": "^3.1.7", - "@types/color": "^3.0.3", "@types/mocha": "^10.0.0", "@types/react": "^18.0.25", "@typescript-eslint/eslint-plugin": "^5.43.0", @@ -547,6 +546,14 @@ "node": ">=14.6" } }, + "node_modules/@ctrl/tinycolor": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.5.0.tgz", + "integrity": "sha512-tlJpwF40DEQcfR/QF+wNMVyGMaO9FQp6Z1Wahj4Gk3CJQYHwA2xVG7iKDFdW6zuxZY9XWOpGcfNCTsX4McOsOg==", + "engines": { + "node": ">=10" + } + }, "node_modules/@custom-elements-manifest/analyzer": { "version": "0.6.6", "resolved": "https://registry.npmjs.org/@custom-elements-manifest/analyzer/-/analyzer-0.6.6.tgz", @@ -1291,30 +1298,6 @@ "@types/qs": "*" } }, - "node_modules/@types/color": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/color/-/color-3.0.3.tgz", - "integrity": "sha512-X//qzJ3d3Zj82J9sC/C18ZY5f43utPbAJ6PhYt/M7uG6etcF6MRpKdN880KBy43B0BMzSfeT96MzrsNjFI3GbA==", - "dev": true, - "dependencies": { - "@types/color-convert": "*" - } - }, - "node_modules/@types/color-convert": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@types/color-convert/-/color-convert-2.0.0.tgz", - "integrity": "sha512-m7GG7IKKGuJUXvkZ1qqG3ChccdIM/qBBo913z+Xft0nKCX4hAU/IxKwZBU4cpRZ7GS5kV4vOblUkILtSShCPXQ==", - "dev": true, - "dependencies": { - "@types/color-name": "*" - } - }, - "node_modules/@types/color-name": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", - "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", - "dev": true - }, "node_modules/@types/command-line-args": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@types/command-line-args/-/command-line-args-5.2.0.tgz", @@ -3982,22 +3965,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/color": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/color/-/color-4.2.1.tgz", - "integrity": "sha512-MFJr0uY4RvTQUKvPq7dh9grVOTYSFeXja2mBXioCGjnjJoXrAp9jJ1NQTDR73c9nwBSAQiNKloKl5zq9WB9UPw==", - "dependencies": { - "color-convert": "^2.0.1", - "color-string": "^1.9.0" - }, - "engines": { - "node": ">=12.5.0" - } - }, "node_modules/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==", + "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -4008,16 +3980,8 @@ "node_modules/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==" - }, - "node_modules/color-string": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.0.tgz", - "integrity": "sha512-9Mrz2AQLefkH1UvASKj6v6hj/7eWgjnT/cVsR8CumieLoT+g900exWeNogqtweI8dxloXN9BDQTYro1oWu/5CQ==", - "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "node_modules/color-support": { "version": "1.1.3", @@ -13336,19 +13300,6 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, - "node_modules/simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", - "dependencies": { - "is-arrayish": "^0.3.1" - } - }, - "node_modules/simple-swizzle/node_modules/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==" - }, "node_modules/sinon": { "version": "14.0.2", "resolved": "https://registry.npmjs.org/sinon/-/sinon-14.0.2.tgz", @@ -15606,6 +15557,11 @@ "integrity": "sha512-OS/t4e5vfUyAiOcyuI1I9d4/EWCx7pA3L8uHNOQQHgjVP41tffMaKTirqRiNhkruIhmxa5Tk5fbQLRMEFapalg==", "dev": true }, + "@ctrl/tinycolor": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.5.0.tgz", + "integrity": "sha512-tlJpwF40DEQcfR/QF+wNMVyGMaO9FQp6Z1Wahj4Gk3CJQYHwA2xVG7iKDFdW6zuxZY9XWOpGcfNCTsX4McOsOg==" + }, "@custom-elements-manifest/analyzer": { "version": "0.6.6", "resolved": "https://registry.npmjs.org/@custom-elements-manifest/analyzer/-/analyzer-0.6.6.tgz", @@ -16210,30 +16166,6 @@ "@types/qs": "*" } }, - "@types/color": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/color/-/color-3.0.3.tgz", - "integrity": "sha512-X//qzJ3d3Zj82J9sC/C18ZY5f43utPbAJ6PhYt/M7uG6etcF6MRpKdN880KBy43B0BMzSfeT96MzrsNjFI3GbA==", - "dev": true, - "requires": { - "@types/color-convert": "*" - } - }, - "@types/color-convert": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@types/color-convert/-/color-convert-2.0.0.tgz", - "integrity": "sha512-m7GG7IKKGuJUXvkZ1qqG3ChccdIM/qBBo913z+Xft0nKCX4hAU/IxKwZBU4cpRZ7GS5kV4vOblUkILtSShCPXQ==", - "dev": true, - "requires": { - "@types/color-name": "*" - } - }, - "@types/color-name": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", - "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", - "dev": true - }, "@types/command-line-args": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@types/command-line-args/-/command-line-args-5.2.0.tgz", @@ -18232,19 +18164,11 @@ } } }, - "color": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/color/-/color-4.2.1.tgz", - "integrity": "sha512-MFJr0uY4RvTQUKvPq7dh9grVOTYSFeXja2mBXioCGjnjJoXrAp9jJ1NQTDR73c9nwBSAQiNKloKl5zq9WB9UPw==", - "requires": { - "color-convert": "^2.0.1", - "color-string": "^1.9.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==", + "dev": true, "requires": { "color-name": "~1.1.4" } @@ -18252,16 +18176,8 @@ "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==" - }, - "color-string": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.0.tgz", - "integrity": "sha512-9Mrz2AQLefkH1UvASKj6v6hj/7eWgjnT/cVsR8CumieLoT+g900exWeNogqtweI8dxloXN9BDQTYro1oWu/5CQ==", - "requires": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "color-support": { "version": "1.1.3", @@ -25255,21 +25171,6 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "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==" - } - } - }, "sinon": { "version": "14.0.2", "resolved": "https://registry.npmjs.org/sinon/-/sinon-14.0.2.tgz", diff --git a/package.json b/package.json index 2309e030..33b23bd8 100644 --- a/package.json +++ b/package.json @@ -62,18 +62,17 @@ "node": ">=14.17.0" }, "dependencies": { + "@ctrl/tinycolor": "^3.5.0", "@floating-ui/dom": "^1.0.7", "@lit-labs/react": "^1.1.0", "@shoelace-style/animations": "^1.1.0", "@shoelace-style/localize": "^3.0.3", - "color": "4.2", "lit": "^2.4.1", "qr-creator": "^1.0.0" }, "devDependencies": { "@custom-elements-manifest/analyzer": "^0.6.6", "@open-wc/testing": "^3.1.7", - "@types/color": "^3.0.3", "@types/mocha": "^10.0.0", "@types/react": "^18.0.25", "@typescript-eslint/eslint-plugin": "^5.43.0", diff --git a/src/components/color-picker/color-picker.ts b/src/components/color-picker/color-picker.ts index 0a163a77..bcee6845 100644 --- a/src/components/color-picker/color-picker.ts +++ b/src/components/color-picker/color-picker.ts @@ -1,4 +1,4 @@ -import Color from 'color'; +import { TinyColor } from '@ctrl/tinycolor'; import { html } from 'lit'; import { customElement, property, query, state } from 'lit/decorators.js'; import { classMap } from 'lit/directives/class-map.js'; @@ -125,10 +125,10 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo @property() label = ''; /** - * The format to use. If opacity is enabled, these will translate to HEXA, RGBA, and HSLA respectively. The color + * The format to use. If opacity is enabled, these will translate to HEXA, RGBA, HSLA and HSVA respectively. The color * picker will accept user input in any format (including CSS color names) and convert it to the desired format. */ - @property() format: 'hex' | 'rgb' | 'hsl' = 'hex'; + @property() format: 'hex' | 'rgb' | 'hsl' | 'hsv' = 'hex'; /** Renders the color picker inline rather than in a dropdown. */ @property({ type: Boolean, reflect: true }) inline = false; @@ -159,7 +159,7 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo /** * An array of predefined color swatches to display. Can include any format the color picker can parse, including - * HEX(A), RGB(A), HSL(A), and CSS color names. + * HEX(A), RGB(A), HSL(A), HSV(A) and CSS color names. */ @property({ attribute: false }) swatches: string[] = [ '#d0021b', @@ -196,7 +196,7 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo } /** Returns the current value as a string in the specified format. */ - getFormattedValue(format: 'hex' | 'hexa' | 'rgb' | 'rgba' | 'hsl' | 'hsla' = 'hex') { + getFormattedValue(format: 'hex' | 'hexa' | 'rgb' | 'rgba' | 'hsl' | 'hsla' | 'hsv' | 'hsva' = 'hex') { const currentColor = this.parseColor( `hsla(${this.hue}, ${this.saturation}%, ${this.lightness}%, ${this.alpha / 100})` ); @@ -218,6 +218,10 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo return currentColor.hsl.string; case 'hsla': return currentColor.hsla.string; + case 'hsv': + return currentColor.hsv.string; + case 'hsva': + return currentColor.hsva.string; default: return ''; } @@ -267,9 +271,9 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo } handleFormatToggle() { - const formats = ['hex', 'rgb', 'hsl']; + const formats = ['hex', 'rgb', 'hsl', 'hsv']; const nextIndex = (formats.indexOf(this.format) + 1) % formats.length; - this.format = formats[nextIndex] as 'hex' | 'rgb' | 'hsl'; + this.format = formats[nextIndex] as 'hex' | 'rgb' | 'hsl' | 'hsv'; this.setColor(this.value); this.emit('sl-change'); this.emit('sl-input'); @@ -507,90 +511,33 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo event.preventDefault(); } - 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?/i.test(colorString)) { - const rgba = colorString - .replace(/[^\d.%]/g, ' ') - .split(' ') - .map(val => val.trim()) - .filter(val => val.length); - - if (rgba.length < 4) { - rgba[3] = '1'; - } - - if (rgba[3].indexOf('%') > -1) { - rgba[3] = (parseFloat(rgba[3].replace(/%/g, '')) / 100).toString(); - } - - return `rgba(${rgba[0]}, ${rgba[1]}, ${rgba[2]}, ${rgba[3]})`; - } - - if (/hsla?/i.test(colorString)) { - const hsla = colorString - .replace(/[^\d.%]/g, ' ') - .split(' ') - .map(val => val.trim()) - .filter(val => val.length); - - if (hsla.length < 4) { - hsla[3] = '1'; - } - - if (hsla[3].indexOf('%') > -1) { - hsla[3] = (parseFloat(hsla[3].replace(/%/g, '')) / 100).toString(); - } - - return `hsla(${hsla[0]}, ${hsla[1]}, ${hsla[2]}, ${hsla[3]})`; - } - - if (/^[0-9a-f]+$/i.test(colorString)) { - return `#${colorString}`; - } - - return colorString; - } - parseColor(colorString: string) { - let parsed: Color; - - // The color module has a weak parser, so we normalize certain things to make the user experience better - colorString = this.normalizeColorString(colorString); - - try { - parsed = Color(colorString); - } catch { + const color = new TinyColor(colorString); + if (!color.isValid) { return null; } - const hslColor = parsed.hsl(); - + const hslColor = color.toHsl(); + // adjust saturation and lightness from 0-1 to 0-100 const hsl = { - h: hslColor.hue(), - s: hslColor.saturationl(), - l: hslColor.lightness(), - a: hslColor.alpha() + h: hslColor.h, + s: hslColor.s * 100, + l: hslColor.l * 100, + a: hslColor.a }; - const rgbColor = parsed.rgb(); + const rgb = color.toRgb(); - const rgb = { - r: rgbColor.red(), - g: rgbColor.green(), - b: rgbColor.blue(), - a: rgbColor.alpha() - }; + const hex = color.toHexString(); + const hexa = color.toHex8String(); - const hex = { - r: toHex(rgb.r), - g: toHex(rgb.g), - b: toHex(rgb.b), - a: toHex(rgb.a * 255) + const hsvColor = color.toHsv(); + // adjust saturation and value from 0-1 to 0-100 + const hsv = { + h: hsvColor.h, + s: hsvColor.s * 100, + v: hsvColor.v * 100, + a: hsvColor.a }; return { @@ -609,6 +556,21 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo `hsla(${Math.round(hsl.h)}, ${Math.round(hsl.s)}%, ${Math.round(hsl.l)}%, ${hsl.a.toFixed(2).toString()})` ) }, + hsv: { + h: hsv.h, + s: hsv.s, + v: hsv.v, + string: this.setLetterCase(`hsv(${Math.round(hsv.h)}, ${Math.round(hsv.s)}%, ${Math.round(hsv.v)}%)`) + }, + hsva: { + h: hsv.h, + s: hsv.s, + v: hsv.v, + a: hsv.a, + string: this.setLetterCase( + `hsva(${Math.round(hsv.h)}, ${Math.round(hsv.s)}%, ${Math.round(hsv.v)}%, ${hsv.a.toFixed(2).toString()})` + ) + }, rgb: { r: rgb.r, g: rgb.g, @@ -624,8 +586,8 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo `rgba(${Math.round(rgb.r)}, ${Math.round(rgb.g)}, ${Math.round(rgb.b)}, ${rgb.a.toFixed(2).toString()})` ) }, - hex: this.setLetterCase(`#${hex.r}${hex.g}${hex.b}`), - hexa: this.setLetterCase(`#${hex.r}${hex.g}${hex.b}${hex.a}`) + hex: this.setLetterCase(hex), + hexa: this.setLetterCase(hexa) }; } @@ -668,6 +630,8 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo this.inputValue = this.opacity ? currentColor.hsla.string : currentColor.hsl.string; } else if (this.format === 'rgb') { this.inputValue = this.opacity ? currentColor.rgba.string : currentColor.rgb.string; + } else if (this.format === 'hsv') { + this.inputValue = this.opacity ? currentColor.hsva.string : currentColor.hsv.string; } else { this.inputValue = this.opacity ? currentColor.hexa : currentColor.hex; } @@ -1005,11 +969,6 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo } } -function toHex(value: number) { - const hex = Math.round(value).toString(16); - return hex.length === 1 ? `0${hex}` : hex; -} - declare global { interface HTMLElementTagNameMap { 'sl-color-picker': SlColorPicker;