mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-30 19:21:54 +00:00
feat(utils): add normalizeColorToHex with guard rails; refactor WidgetColorPicker to use it; tests for invalid inputs and coercion
This commit is contained in:
@@ -29,7 +29,7 @@ import { computed } from 'vue'
|
|||||||
|
|
||||||
import { useWidgetValue } from '@/composables/graph/useWidgetValue'
|
import { useWidgetValue } from '@/composables/graph/useWidgetValue'
|
||||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||||
import { hsbToRgb, parseToRgb, rgbToHex } from '@/utils/colorUtil'
|
import { normalizeColorToHex } from '@/utils/colorUtil'
|
||||||
import { cn } from '@/utils/tailwindUtil'
|
import { cn } from '@/utils/tailwindUtil'
|
||||||
import {
|
import {
|
||||||
PANEL_EXCLUDED_PROPS,
|
PANEL_EXCLUDED_PROPS,
|
||||||
@@ -49,59 +49,13 @@ const emit = defineEmits<{
|
|||||||
'update:modelValue': [value: string]
|
'update:modelValue': [value: string]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
// Use the composable for consistent widget value handling
|
|
||||||
function normalizeToHexWithHash(value: unknown): string {
|
|
||||||
if (typeof value === 'string') {
|
|
||||||
const raw = value.trim()
|
|
||||||
// Bare hex without '#'
|
|
||||||
if (/^[0-9a-fA-F]{3}$/.test(raw) || /^[0-9a-fA-F]{6}$/.test(raw)) {
|
|
||||||
return `#${raw.toLowerCase()}`
|
|
||||||
}
|
|
||||||
// If starts with '#', ensure lower-case and valid length
|
|
||||||
if (raw.startsWith('#')) {
|
|
||||||
const hex = raw.toLowerCase()
|
|
||||||
if (hex.length === 4 || hex.length === 7) return hex
|
|
||||||
// Fallback: attempt parse via RGB and re-encode
|
|
||||||
}
|
|
||||||
// rgb(), rgba(), hsl(), hsla()
|
|
||||||
if (/^(rgb|rgba|hsl|hsla)\(/i.test(raw)) {
|
|
||||||
const rgb = parseToRgb(raw)
|
|
||||||
return rgbToHex(rgb).toLowerCase()
|
|
||||||
}
|
|
||||||
// hsb(h,s,b)
|
|
||||||
if (/^hsb\(/i.test(raw)) {
|
|
||||||
const nums = raw.match(/\d+(?:\.\d+)?/g)?.map(Number) || []
|
|
||||||
if (nums.length >= 3) {
|
|
||||||
const rgb = hsbToRgb({ h: nums[0], s: nums[1], b: nums[2] })
|
|
||||||
return rgbToHex(rgb).toLowerCase()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// HSB object from PrimeVue
|
|
||||||
if (
|
|
||||||
value &&
|
|
||||||
typeof value === 'object' &&
|
|
||||||
'h' in (value as any) &&
|
|
||||||
's' in (value as any) &&
|
|
||||||
('b' in (value as any) || 'v' in (value as any))
|
|
||||||
) {
|
|
||||||
const h = Number((value as any).h)
|
|
||||||
const s = Number((value as any).s)
|
|
||||||
const b = Number((value as any).b ?? (value as any).v)
|
|
||||||
const rgb = hsbToRgb({ h, s, b })
|
|
||||||
return rgbToHex(rgb).toLowerCase()
|
|
||||||
}
|
|
||||||
// Fallback to default black
|
|
||||||
return '#000000'
|
|
||||||
}
|
|
||||||
|
|
||||||
const { localValue, onChange } = useWidgetValue({
|
const { localValue, onChange } = useWidgetValue({
|
||||||
widget: props.widget,
|
widget: props.widget,
|
||||||
// Normalize initial model value to ensure leading '#'
|
// Normalize initial model value to ensure leading '#'
|
||||||
modelValue: normalizeToHexWithHash(props.modelValue),
|
modelValue: normalizeColorToHex(props.modelValue),
|
||||||
defaultValue: '#000000',
|
defaultValue: '#000000',
|
||||||
emit,
|
emit,
|
||||||
transform: (val: unknown) => normalizeToHexWithHash(val)
|
transform: (val: unknown) => normalizeColorToHex(val)
|
||||||
})
|
})
|
||||||
|
|
||||||
// ColorPicker specific excluded props include panel/overlay classes
|
// ColorPicker specific excluded props include panel/overlay classes
|
||||||
|
|||||||
@@ -115,6 +115,73 @@ export function hsbToRgb({ h, s, b }: HSB): RGB {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize various color inputs (hex, rgb/rgba, hsl/hsla, hsb string/object)
|
||||||
|
* into lowercase #rrggbb. Falls back to #000000 on invalid inputs.
|
||||||
|
*/
|
||||||
|
export function normalizeColorToHex(value: unknown): string {
|
||||||
|
// String inputs
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const raw = value.trim()
|
||||||
|
if (!raw) return '#000000'
|
||||||
|
|
||||||
|
// Bare hex without '#'
|
||||||
|
if (/^[0-9a-fA-F]{3}$/.test(raw) || /^[0-9a-fA-F]{6}$/.test(raw)) {
|
||||||
|
return `#${raw.toLowerCase()}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Starts with '#': only accept #rgb or #rrggbb
|
||||||
|
if (raw.startsWith('#')) {
|
||||||
|
const hex = raw.toLowerCase()
|
||||||
|
if (hex.length === 4 || hex.length === 7) return hex
|
||||||
|
return '#000000'
|
||||||
|
}
|
||||||
|
|
||||||
|
// rgb(), rgba(), hsl(), hsla()
|
||||||
|
if (/^(rgb|rgba|hsl|hsla)\(/i.test(raw)) {
|
||||||
|
const rgb = parseToRgb(raw)
|
||||||
|
return rgbToHex(rgb).toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
// hsb(h,s,b)
|
||||||
|
if (/^hsb\(/i.test(raw)) {
|
||||||
|
const nums = raw.match(/\d+(?:\.\d+)?/g)?.map(Number) || []
|
||||||
|
if (nums.length >= 3) {
|
||||||
|
const [h, s, b] = nums
|
||||||
|
if ([h, s, b].every((n) => Number.isFinite(n))) {
|
||||||
|
const rgb = hsbToRgb({ h, s, b })
|
||||||
|
return rgbToHex(rgb).toLowerCase()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '#000000'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unknown string format -> default
|
||||||
|
return '#000000'
|
||||||
|
}
|
||||||
|
|
||||||
|
// HSB object (PrimeVue)
|
||||||
|
if (
|
||||||
|
value &&
|
||||||
|
typeof value === 'object' &&
|
||||||
|
'h' in (value as any) &&
|
||||||
|
's' in (value as any) &&
|
||||||
|
('b' in (value as any) || 'v' in (value as any))
|
||||||
|
) {
|
||||||
|
const h = Number((value as any).h)
|
||||||
|
const s = Number((value as any).s)
|
||||||
|
const b = Number((value as any).b ?? (value as any).v)
|
||||||
|
if ([h, s, b].every((n) => Number.isFinite(n))) {
|
||||||
|
const rgb = hsbToRgb({ h, s, b })
|
||||||
|
return rgbToHex(rgb).toLowerCase()
|
||||||
|
}
|
||||||
|
return '#000000'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback
|
||||||
|
return '#000000'
|
||||||
|
}
|
||||||
|
|
||||||
export function parseToRgb(color: string): RGB {
|
export function parseToRgb(color: string): RGB {
|
||||||
const format = identifyColorFormat(color)
|
const format = identifyColorFormat(color)
|
||||||
if (!format) return { r: 0, g: 0, b: 0 }
|
if (!format) return { r: 0, g: 0, b: 0 }
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
import { hexToRgb, hsbToRgb, parseToRgb, rgbToHex } from '@/utils/colorUtil'
|
import {
|
||||||
|
hexToRgb,
|
||||||
|
hsbToRgb,
|
||||||
|
normalizeColorToHex,
|
||||||
|
parseToRgb,
|
||||||
|
rgbToHex
|
||||||
|
} from '@/utils/colorUtil'
|
||||||
|
|
||||||
describe('colorUtil conversions', () => {
|
describe('colorUtil conversions', () => {
|
||||||
describe('hexToRgb / rgbToHex', () => {
|
describe('hexToRgb / rgbToHex', () => {
|
||||||
@@ -69,4 +75,31 @@ describe('colorUtil conversions', () => {
|
|||||||
expect(rgbToHex(rgb)).toBe('#7f0000')
|
expect(rgbToHex(rgb)).toBe('#7f0000')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('normalizeColorToHex (guard rails)', () => {
|
||||||
|
it('returns #hex for common inputs and falls back to black', () => {
|
||||||
|
expect(normalizeColorToHex('#FFaa00')).toBe('#ffaa00')
|
||||||
|
expect(normalizeColorToHex('ffaa00')).toBe('#ffaa00')
|
||||||
|
expect(normalizeColorToHex('rgb(300, -5, 16)')).toBe('#ff0010')
|
||||||
|
expect(normalizeColorToHex('hsl(0, 100%, 50%)')).toBe('#ff0000')
|
||||||
|
expect(normalizeColorToHex('hsb(120, 100, 100)')).toBe('#00ff00')
|
||||||
|
|
||||||
|
// invalid strings
|
||||||
|
expect(normalizeColorToHex('')).toBe('#000000')
|
||||||
|
expect(normalizeColorToHex(' ')).toBe('#000000')
|
||||||
|
expect(normalizeColorToHex('#12')).toBe('#000000')
|
||||||
|
expect(normalizeColorToHex('#zzzzzz')).toBe('#000000')
|
||||||
|
expect(normalizeColorToHex('not-a-color')).toBe('#000000')
|
||||||
|
|
||||||
|
// HSB object inputs
|
||||||
|
expect(normalizeColorToHex({ h: 240, s: 100, b: 100 } as any)).toBe(
|
||||||
|
'#0000ff'
|
||||||
|
)
|
||||||
|
expect(normalizeColorToHex({ h: NaN, s: 100, b: 100 } as any)).toBe(
|
||||||
|
'#000000'
|
||||||
|
)
|
||||||
|
expect(normalizeColorToHex(null)).toBe('#000000')
|
||||||
|
expect(normalizeColorToHex(undefined)).toBe('#000000')
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user