diff --git a/packages/components/color-picker/__tests__/color-picker.test.tsx b/packages/components/color-picker/__tests__/color-picker.test.tsx index e6f5cbcd15..cd6bb6aca3 100644 --- a/packages/components/color-picker/__tests__/color-picker.test.tsx +++ b/packages/components/color-picker/__tests__/color-picker.test.tsx @@ -35,7 +35,7 @@ describe('Color-picker', () => { wrapper.unmount() }) it('should show alpha slider when show-alpha=true', async () => { - const color = ref('#20A0FF') + const color = ref('#20a0ff') const wrapper = mount(() => ( )) @@ -46,7 +46,7 @@ describe('Color-picker', () => { wrapper.unmount() }) it('should show correct rgb value', async () => { - const color = ref('#20A0FF') + const color = ref('#20a0ff') const wrapper = mount(() => ) await wrapper.find('.el-color-picker__trigger').trigger('click') @@ -54,11 +54,11 @@ describe('Color-picker', () => { const input = document.querySelector( '.el-color-dropdown__value input' ) - expect(input!.value.trim().toUpperCase()).toEqual('#20A0FF') + expect(input!.value.trim()).toEqual('#20a0ff') wrapper.unmount() }) it('should show correct hex value contains alpha', async () => { - const color = ref('#20A0FFEE') + const color = ref('#20a0ffee') const wrapper = mount(() => ( )) @@ -68,7 +68,7 @@ describe('Color-picker', () => { const input = document.querySelector( '.el-color-dropdown__value input' ) - expect(input!.value.trim().toUpperCase()).toEqual('#20A0FFEE') + expect(input!.value.trim()).toEqual('#20a0ffee') wrapper.unmount() }) it('control alpha changes through keyboard', async () => { @@ -106,7 +106,7 @@ describe('Color-picker', () => { await wrapper.find('.el-color-picker__trigger').trigger('click') document.querySelector('.el-color-dropdown__btn')?.click() await nextTick() - expect(color.value).toEqual('#FF0000') + expect(color.value).toEqual('') // should be empty #13239 wrapper.unmount() }) it('should pick a color contains alpha when confirm button click', async () => { @@ -118,11 +118,11 @@ describe('Color-picker', () => { await wrapper.find('.el-color-picker__trigger').trigger('click') document.querySelector('.el-color-dropdown__btn')?.click() await nextTick() - expect(color.value).toEqual('#FF0000FF') + expect(color.value).toEqual('') // should be empty #13239 wrapper.unmount() }) it('should init the right color when open', async () => { - const color = ref('#0F0') + const color = ref('#0f0') const wrapper = mount(() => ) const colorPickerWrapper = wrapper.findComponent(ColorPicker) @@ -148,7 +148,7 @@ describe('Color-picker', () => { wrapper.unmount() }) it('should show color picker when click trigger', async () => { - const color = ref('#0F0') + const color = ref('#0f0') const wrapper = mount(() => ) await wrapper.find('.el-color-picker__trigger').trigger('click') @@ -157,7 +157,7 @@ describe('Color-picker', () => { wrapper.unmount() }) it('should clear a color when clear button click', async () => { - const color = ref('#0F0') + const color = ref('#0f0') const wrapper = mount(() => ) await wrapper.find('.el-color-picker__trigger').trigger('click') @@ -169,7 +169,7 @@ describe('Color-picker', () => { wrapper.unmount() }) it('should change hue when clicking the hue bar', async () => { - const color = ref('#F00') + const color = ref('#f00') const wrapper = mount(() => ) await wrapper.find('.el-color-picker__trigger').trigger('click') @@ -204,7 +204,7 @@ describe('Color-picker', () => { wrapper.unmount() }) it('should change hue when saturation is zero', async () => { - const color = ref('20A0FF') + const color = ref('20a0ff') const wrapper = mount(() => ) await wrapper.find('.el-color-picker__trigger').trigger('click') @@ -244,7 +244,7 @@ describe('Color-picker', () => { wrapper.unmount() }) it('should change alpha when clicking the alpha bar', async () => { - const color = ref('#F00') + const color = ref('#f00') const wrapper = mount(() => ( )) @@ -423,7 +423,7 @@ describe('Color-picker', () => { }) it('should not execute activeChange event', async () => { const onActiveChange = vi.fn() - const color = ref('#20A0FF') + const color = ref('#20a0ff') const wrapper = mount(() => ( )) @@ -434,7 +434,7 @@ describe('Color-picker', () => { }) it('should update the colorFormat and selected color when the colorFormat prop changes', async () => { - const color = ref('#00FF00') + const color = ref('#00ff00') const colorFormat = ref('hex') const wrapper = mount(() => ( @@ -454,7 +454,7 @@ describe('Color-picker', () => { wrapper.unmount() }) it('should update the selected color when the showAlpha prop changes', async () => { - const color = ref('#00FF00AA') + const color = ref('#00ff00aa') const showAlpha = ref(true) const wrapper = mount(() => ( { ref: 'inputRef', }) expect(colorPickerWrapper.vm.color.enableAlpha).toBe(false) - expect(color.value).toBe('#00FF00') + expect(color.value).toBe('#00ff00') expect( customInput.find('.el-input__inner').element.value - ).toBe('#00FF00') + ).toBe('#00ff00') wrapper.unmount() }) diff --git a/packages/components/color-picker/src/color-picker.vue b/packages/components/color-picker/src/color-picker.vue index 9b85548acb..3e595384ff 100644 --- a/packages/components/color-picker/src/color-picker.vue +++ b/packages/components/color-picker/src/color-picker.vue @@ -39,8 +39,7 @@ v-model="customInput" :validate-event="false" size="small" - @keyup.enter="handleConfirm" - @blur="handleConfirm" + @change="handleConfirm" /> { }) function displayedRgb(color: Color, showAlpha: boolean) { - if (!(color instanceof Color)) { - throw new TypeError('color should be instance of _color Class') - } - - const { r, g, b } = color.toRgb() - return showAlpha - ? `rgba(${r}, ${g}, ${b}, ${color.get('alpha') / 100})` - : `rgb(${r}, ${g}, ${b})` + const { r, g, b, a } = color.toRgb() + return showAlpha ? `rgba(${r}, ${g}, ${b}, ${a})` : `rgb(${r}, ${g}, ${b})` } function setShowPicker(value: boolean) { diff --git a/packages/components/color-picker/src/components/predefine.vue b/packages/components/color-picker/src/components/predefine.vue index 9d440a573d..105c8ae7f4 100644 --- a/packages/components/color-picker/src/components/predefine.vue +++ b/packages/components/color-picker/src/components/predefine.vue @@ -6,7 +6,7 @@ :key="colors[index]" :class="[ ns.e('color-selector'), - ns.is('alpha', item._alpha < 100), + ns.is('alpha', item.get('alpha') < 100), { selected: item.selected }, ]" @click="handleSelect(index)" @@ -51,8 +51,9 @@ export default defineComponent({ watch( () => currentColor.value, (val) => { - const color = new Color() - color.fromString(val) + const color = new Color({ + value: val, + }) rgbaColors.value.forEach((item) => { item.selected = color.compare(item) @@ -70,11 +71,10 @@ export default defineComponent({ function parseColors(colors: string[], color: Color) { return colors.map((value) => { - const c = new Color() - c.enableAlpha = props.enableAlpha - c.format = 'rgba' - c.fromString(value) - c.selected = c.value === color.value + const c = new Color({ + value, + }) + c.selected = c.compare(color) return c }) } diff --git a/packages/components/color-picker/src/utils/color.ts b/packages/components/color-picker/src/utils/color.ts index 84bc45805c..03934644fc 100644 --- a/packages/components/color-picker/src/utils/color.ts +++ b/packages/components/color-picker/src/utils/color.ts @@ -1,172 +1,6 @@ -import { hasOwn, isString } from '@element-plus/utils' - -const hsv2hsl = function (hue: number, sat: number, val: number) { - return [ - hue, - (sat * val) / ((hue = (2 - sat) * val) < 1 ? hue : 2 - hue) || 0, - hue / 2, - ] -} - -// Need to handle 1.0 as 100%, since once it is a number, there is no difference between it and 1 -// -const isOnePointZero = function (n: unknown) { - return isString(n) && n.includes('.') && Number.parseFloat(n) === 1 -} - -const isPercentage = function (n: unknown) { - return isString(n) && n.includes('%') -} - -// Take input from [0, n] and return it as [0, 1] -const bound01 = function (value: number | string, max: number | string) { - if (isOnePointZero(value)) value = '100%' - - const processPercent = isPercentage(value) - value = Math.min(max as number, Math.max(0, Number.parseFloat(`${value}`))) - - // Automatically convert percentage into number - if (processPercent) { - value = Number.parseInt(`${value * (max as number)}`, 10) / 100 - } - - // Handle floating point rounding errors - if (Math.abs(value - (max as number)) < 0.000001) { - return 1 - } - - // Convert into [0, 1] range if it isn't already - return (value % (max as number)) / Number.parseFloat(max as string) -} - -const INT_HEX_MAP: Record = { - 10: 'A', - 11: 'B', - 12: 'C', - 13: 'D', - 14: 'E', - 15: 'F', -} - -const hexOne = (value: number) => { - value = Math.min(Math.round(value), 255) - const high = Math.floor(value / 16) - const low = value % 16 - return `${INT_HEX_MAP[high] || high}${INT_HEX_MAP[low] || low}` -} - -const toHex = function ({ r, g, b }: { r: number; g: number; b: number }) { - if (Number.isNaN(+r) || Number.isNaN(+g) || Number.isNaN(+b)) return '' - return `#${hexOne(r)}${hexOne(g)}${hexOne(b)}` -} - -const HEX_INT_MAP: Record = { - A: 10, - B: 11, - C: 12, - D: 13, - E: 14, - F: 15, -} - -const parseHexChannel = function (hex: string) { - if (hex.length === 2) { - return ( - (HEX_INT_MAP[hex[0].toUpperCase()] || +hex[0]) * 16 + - (HEX_INT_MAP[hex[1].toUpperCase()] || +hex[1]) - ) - } - - return HEX_INT_MAP[hex[1].toUpperCase()] || +hex[1] -} - -const hsl2hsv = function (hue: number, sat: number, light: number) { - sat = sat / 100 - light = light / 100 - let smin = sat - const lmin = Math.max(light, 0.01) - // let sv - // let v - - light *= 2 - sat *= light <= 1 ? light : 2 - light - smin *= lmin <= 1 ? lmin : 2 - lmin - const v = (light + sat) / 2 - const sv = - light === 0 ? (2 * smin) / (lmin + smin) : (2 * sat) / (light + sat) - - return { - h: hue, - s: sv * 100, - v: v * 100, - } -} - -// `rgbToHsv` -// Converts an RGB color value to HSV -// *Assumes:* r, g, and b are contained in the set [0, 255] or [0, 1] -// *Returns:* { h, s, v } in [0,1] -const rgb2hsv = (r: number, g: number, b: number) => { - r = bound01(r, 255) - g = bound01(g, 255) - b = bound01(b, 255) - - const max = Math.max(r, g, b) - const min = Math.min(r, g, b) - let h: number - const v = max - - const d = max - min - const s = max === 0 ? 0 : d / max - - if (max === min) { - h = 0 // achromatic - } else { - switch (max) { - case r: { - h = (g - b) / d + (g < b ? 6 : 0) - break - } - case g: { - h = (b - r) / d + 2 - break - } - case b: { - h = (r - g) / d + 4 - break - } - } - h! /= 6 - } - - return { h: h! * 360, s: s * 100, v: v * 100 } -} - -// `hsvToRgb` -// Converts an HSV color value to RGB. -// *Assumes:* h is contained in [0, 1] or [0, 360] and s and v are contained in [0, 1] or [0, 100] -// *Returns:* { r, g, b } in the set [0, 255] -const hsv2rgb = function (h: number, s: number, v: number) { - h = bound01(h, 360) * 6 - s = bound01(s, 100) - v = bound01(v, 100) - - const i = Math.floor(h) - const f = h - i - const p = v * (1 - s) - const q = v * (1 - f * s) - const t = v * (1 - (1 - f) * s) - const mod = i % 6 - const r = [v, q, p, p, t, v][mod] - const g = [t, v, v, q, p, p][mod] - const b = [p, p, t, v, v, q][mod] - - return { - r: Math.round(r * 255), - g: Math.round(g * 255), - b: Math.round(b * 255), - } -} +import { TinyColor } from '@ctrl/tinycolor' +import { hasOwn } from '@element-plus/utils' +import type { ColorFormats } from '@ctrl/tinycolor' interface ColorOptions { enableAlpha: boolean @@ -178,9 +12,11 @@ export default class Color { private _hue = 0 private _saturation = 100 private _value = 100 - _alpha = 100 + private _alpha = 100 + private _tiny = new TinyColor() + private _isValid = false public enableAlpha = false - public format = 'hex' + public format = '' public value = '' public selected?: boolean @@ -207,185 +43,64 @@ export default class Color { return } - ;(this as any)[`_${prop}`] = value + this._isValid = true this.doOnChange() } get(prop: string) { - if (prop === 'alpha') { - return Math.floor(this[`_${prop}`]) + if (['hue', 'saturation', 'value', 'alpha'].includes(prop)) { + return Math.round((this as any)[`_${prop}`]) } return (this as any)[`_${prop}`] } toRgb() { - return hsv2rgb(this._hue, this._saturation, this._value) + return this._isValid ? this._tiny.toRgb() : { r: 255, g: 255, b: 255, a: 0 } } fromString(value: string) { - if (!value) { + const color = new TinyColor(value) + this._isValid = color.isValid + if (color.isValid) { + const { h, s, v, a } = color.toHsv() + this._hue = h + this._saturation = s * 100 + this._value = v * 100 + this._alpha = a * 100 + } else { this._hue = 0 this._saturation = 100 this._value = 100 - - this.doOnChange() - return - } - - const fromHSV = (h: number, s: number, v: number) => { - this._hue = Math.max(0, Math.min(360, h)) - this._saturation = Math.max(0, Math.min(100, s)) - this._value = Math.max(0, Math.min(100, v)) - - this.doOnChange() - } - - if (value.includes('hsl')) { - const parts = value - .replace(/hsla|hsl|\(|\)/gm, '') - .split(/\s|,/g) - .filter((val) => val !== '') - .map((val, index) => - index > 2 ? Number.parseFloat(val) : Number.parseInt(val, 10) - ) - - if (parts.length === 4) { - // @ts-expect-error - this._alpha = Number.parseFloat(parts[3]) * 100 - } else if (parts.length === 3) { - this._alpha = 100 - } - if (parts.length >= 3) { - const { h, s, v } = hsl2hsv(parts[0], parts[1], parts[2]) - fromHSV(h, s, v) - } - } else if (value.includes('hsv')) { - const parts = value - .replace(/hsva|hsv|\(|\)/gm, '') - .split(/\s|,/g) - .filter((val) => val !== '') - .map((val, index) => - index > 2 ? Number.parseFloat(val) : Number.parseInt(val, 10) - ) - - if (parts.length === 4) { - // @ts-expect-error - this._alpha = Number.parseFloat(parts[3]) * 100 - } else if (parts.length === 3) { - this._alpha = 100 - } - if (parts.length >= 3) { - fromHSV(parts[0], parts[1], parts[2]) - } - } else if (value.includes('rgb')) { - const parts = value - .replace(/rgba|rgb|\(|\)/gm, '') - .split(/\s|,/g) - .filter((val) => val !== '') - .map((val, index) => - index > 2 ? Number.parseFloat(val) : Number.parseInt(val, 10) - ) - - if (parts.length === 4) { - // @ts-expect-error - this._alpha = Number.parseFloat(parts[3]) * 100 - } else if (parts.length === 3) { - this._alpha = 100 - } - if (parts.length >= 3) { - const { h, s, v } = rgb2hsv(parts[0], parts[1], parts[2]) - fromHSV(h, s, v) - } - } else if (value.includes('#')) { - const hex = value.replace('#', '').trim() - if (!/^[0-9a-fA-F]{3}$|^[0-9a-fA-F]{6}$|^[0-9a-fA-F]{8}$/.test(hex)) - return - let r: number, g: number, b: number - - if (hex.length === 3) { - r = parseHexChannel(hex[0] + hex[0]) - g = parseHexChannel(hex[1] + hex[1]) - b = parseHexChannel(hex[2] + hex[2]) - } else if (hex.length === 6 || hex.length === 8) { - r = parseHexChannel(hex.slice(0, 2)) - g = parseHexChannel(hex.slice(2, 4)) - b = parseHexChannel(hex.slice(4, 6)) - } - - if (hex.length === 8) { - this._alpha = (parseHexChannel(hex.slice(6)) / 255) * 100 - } else if (hex.length === 3 || hex.length === 6) { - this._alpha = 100 - } - - const { h, s, v } = rgb2hsv(r!, g!, b!) - fromHSV(h, s, v) + this._alpha = 100 } + this.doOnChange() } compare(color: this) { - return ( - Math.abs(color._hue - this._hue) < 2 && - Math.abs(color._saturation - this._saturation) < 1 && - Math.abs(color._value - this._value) < 1 && - Math.abs(color._alpha - this._alpha) < 1 - ) + const compareColor = new TinyColor({ + h: color._hue, + s: color._saturation / 100, + v: color._value / 100, + a: color._alpha / 100, + }) + return this._tiny.equals(compareColor) } doOnChange() { - const { _hue, _saturation, _value, _alpha, format } = this - - if (this.enableAlpha) { - switch (format) { - case 'hsl': { - const hsl = hsv2hsl(_hue, _saturation / 100, _value / 100) - this.value = `hsla(${_hue}, ${Math.round( - hsl[1] * 100 - )}%, ${Math.round(hsl[2] * 100)}%, ${this.get('alpha') / 100})` - break - } - case 'hsv': { - this.value = `hsva(${_hue}, ${Math.round(_saturation)}%, ${Math.round( - _value - )}%, ${this.get('alpha') / 100})` - break - } - case 'hex': { - this.value = `${toHex(hsv2rgb(_hue, _saturation, _value))}${hexOne( - (_alpha * 255) / 100 - )}` - break - } - default: { - const { r, g, b } = hsv2rgb(_hue, _saturation, _value) - this.value = `rgba(${r}, ${g}, ${b}, ${this.get('alpha') / 100})` - } - } - } else { - switch (format) { - case 'hsl': { - const hsl = hsv2hsl(_hue, _saturation / 100, _value / 100) - this.value = `hsl(${_hue}, ${Math.round(hsl[1] * 100)}%, ${Math.round( - hsl[2] * 100 - )}%)` - break - } - case 'hsv': { - this.value = `hsv(${_hue}, ${Math.round(_saturation)}%, ${Math.round( - _value - )}%)` - break - } - case 'rgb': { - const { r, g, b } = hsv2rgb(_hue, _saturation, _value) - this.value = `rgb(${r}, ${g}, ${b})` - break - } - default: { - this.value = toHex(hsv2rgb(_hue, _saturation, _value)) - } - } + const { _hue, _saturation, _value, _alpha, format, enableAlpha } = this + let _format = format || (enableAlpha ? 'rgb' : 'hex') + if (format === 'hex' && enableAlpha) { + _format = 'hex8' } + this._tiny = new TinyColor({ + h: _hue, + s: _saturation / 100, + v: _value / 100, + a: _alpha / 100, + }) + this.value = this._isValid + ? this._tiny.toString(_format as ColorFormats) + : '' } }