From 39e8611a59ea06e0a8272e3176fc3c69bff2a341 Mon Sep 17 00:00:00 2001 From: CCherry07 <2405693142@qq.com> Date: Sun, 9 Jul 2023 09:54:55 +0800 Subject: [PATCH 01/23] feat: vc-color-picker --- components/color-picker/index.tsx | 1 + components/vc-color-picker/ColorPicker.tsx | 125 ++++++++++++++++ components/vc-color-picker/color.ts | 53 +++++++ .../vc-color-picker/components/ColorBlock.tsx | 32 +++++ .../vc-color-picker/components/Gradient.tsx | 46 ++++++ .../vc-color-picker/components/Handler.tsx | 29 ++++ .../vc-color-picker/components/Palette.tsx | 20 +++ .../vc-color-picker/components/Picker.tsx | 62 ++++++++ .../vc-color-picker/components/Slider.tsx | 84 +++++++++++ .../vc-color-picker/components/Transform.tsx | 33 +++++ .../vc-color-picker/hooks/useColorDrag.ts | 134 ++++++++++++++++++ .../vc-color-picker/hooks/useColorState.ts | 40 ++++++ components/vc-color-picker/index.tsx | 8 ++ components/vc-color-picker/interface.ts | 38 +++++ components/vc-color-picker/util.ts | 99 +++++++++++++ 15 files changed, 804 insertions(+) create mode 100644 components/color-picker/index.tsx create mode 100644 components/vc-color-picker/ColorPicker.tsx create mode 100644 components/vc-color-picker/color.ts create mode 100644 components/vc-color-picker/components/ColorBlock.tsx create mode 100644 components/vc-color-picker/components/Gradient.tsx create mode 100644 components/vc-color-picker/components/Handler.tsx create mode 100644 components/vc-color-picker/components/Palette.tsx create mode 100644 components/vc-color-picker/components/Picker.tsx create mode 100644 components/vc-color-picker/components/Slider.tsx create mode 100644 components/vc-color-picker/components/Transform.tsx create mode 100644 components/vc-color-picker/hooks/useColorDrag.ts create mode 100644 components/vc-color-picker/hooks/useColorState.ts create mode 100644 components/vc-color-picker/index.tsx create mode 100644 components/vc-color-picker/interface.ts create mode 100644 components/vc-color-picker/util.ts diff --git a/components/color-picker/index.tsx b/components/color-picker/index.tsx new file mode 100644 index 0000000000..d5a2fe0400 --- /dev/null +++ b/components/color-picker/index.tsx @@ -0,0 +1 @@ +// import ColorPicker from '../vc-color-picker'; diff --git a/components/vc-color-picker/ColorPicker.tsx b/components/vc-color-picker/ColorPicker.tsx new file mode 100644 index 0000000000..00a6e3df11 --- /dev/null +++ b/components/vc-color-picker/ColorPicker.tsx @@ -0,0 +1,125 @@ +import { defineComponent, type CSSProperties, computed, shallowRef } from 'vue'; + +import { ColorPickerPrefixCls, defaultColor, generateColor } from './util'; + +import ColorBlock from './components/ColorBlock'; +import Picker from './components/Picker'; +import Slider from './components/Slider'; +import useColorState from './hooks/useColorState'; +import type { BaseColorPickerProps, ColorGenInput } from './interface'; +import type { VueNode } from '../_util/type'; +import classNames from '../_util/classNames'; + +const hueColor = [ + 'rgb(255, 0, 0) 0%', + 'rgb(255, 255, 0) 17%', + 'rgb(0, 255, 0) 33%', + 'rgb(0, 255, 255) 50%', + 'rgb(0, 0, 255) 67%', + 'rgb(255, 0, 255) 83%', + 'rgb(255, 0, 0) 100%', +]; + +export interface ColorPickerProps extends BaseColorPickerProps { + value?: ColorGenInput; + defaultValue?: ColorGenInput; + className?: string; + style?: CSSProperties; + /** Get panel element */ + panelRender?: (panel: VueNode) => VueNode; + /** Disabled alpha selection */ + disabledAlpha?: boolean; +} + +const ColorPicker = defineComponent({ + name: 'AColorPicker', + props: [ + 'value', + 'defaultValue', + 'prefixCls', + 'onChange', + 'onChangeComplete', + 'panelRender', + 'disabledAlpha', + 'disabled', + ], + emits: ['change', 'changeComplete'], + setup(props, { expose }) { + const disabledAlpha = computed(() => props.disabledAlpha || false); + const disabled = computed(() => props.disabled || false); + const prefixCls = computed(() => props.prefixCls || ColorPickerPrefixCls); + const value = computed(() => props.value); + const colorPickerRef = shallowRef(); + const [colorValue, setColorValue] = useColorState(defaultColor, { + value, + defaultValue: props.defaultValue, + }); + + const alphaColor = computed(() => { + const rgb = generateColor(colorValue.value.toRgbString()); + // alpha color need equal 1 for base color + rgb.setAlpha(1); + return rgb.toRgbString(); + }); + const mergeCls = classNames(`${prefixCls.value}-panel`, { + [`${prefixCls.value}-panel-disabled`]: disabled.value, + }); + const basicProps = computed(() => ({ + prefixCls: prefixCls.value, + onChangeComplete: props.onChangeComplete, + disabled: disabled.value, + })); + + const handleChange: BaseColorPickerProps['onChange'] = (data, type) => { + if (!props.value) { + setColorValue(data); + } + props.onChange?.(data, type); + }; + const defaultPanel = computed(() => ( + <> + +
+
+ handleChange(color, 'hue')} + {...basicProps.value} + /> + {!disabledAlpha.value && ( + handleChange(color, 'alpha')} + {...basicProps.value} + /> + )} +
+ +
+ + )); + expose({ + colorPickerRef, + }); + return () => { + return ( +
+ {typeof props.panelRender === 'function' + ? props.panelRender(defaultPanel.value) + : defaultPanel.value} +
+ ); + }; + }, +}); + +export default ColorPicker; diff --git a/components/vc-color-picker/color.ts b/components/vc-color-picker/color.ts new file mode 100644 index 0000000000..9ab847d1a2 --- /dev/null +++ b/components/vc-color-picker/color.ts @@ -0,0 +1,53 @@ +import type { ColorInput, HSVA, Numberify } from '@ctrl/tinycolor'; +import { TinyColor } from '@ctrl/tinycolor'; +import type { ColorGenInput, HSB, HSBA } from './interface'; + +export const getRoundNumber = (value: number) => Math.round(Number(value || 0)); + +const convertHsb2Hsv = (color: ColorGenInput): ColorInput => { + if (color && typeof color === 'object' && 'h' in color && 'b' in color) { + const { b, ...resets } = color as HSB; + return { + ...resets, + v: b, + }; + } + if (typeof color === 'string' && /hsb/.test(color)) { + return color.replace(/hsb/, 'hsv'); + } + return color as ColorInput; +}; + +export class Color extends TinyColor { + constructor(color: ColorGenInput) { + super(convertHsb2Hsv(color)); + } + + toHsbString() { + const hsb = this.toHsb(); + const saturation = getRoundNumber(hsb.s * 100); + const lightness = getRoundNumber(hsb.b * 100); + const hue = getRoundNumber(hsb.h); + const alpha = hsb.a; + const hsbString = `hsb(${hue}, ${saturation}%, ${lightness}%)`; + const hsbaString = `hsba(${hue}, ${saturation}%, ${lightness}%, ${alpha.toFixed( + alpha === 0 ? 0 : 2, + )})`; + return alpha === 1 ? hsbString : hsbaString; + } + + toHsb(): Numberify { + let hsv = this.toHsv(); + if (typeof this.originalInput === 'object' && this.originalInput) { + if ('h' in this.originalInput) { + hsv = this.originalInput as Numberify; + } + } + + const { v, ...resets } = hsv; + return { + ...resets, + b: hsv.v, + }; + } +} diff --git a/components/vc-color-picker/components/ColorBlock.tsx b/components/vc-color-picker/components/ColorBlock.tsx new file mode 100644 index 0000000000..d166183cea --- /dev/null +++ b/components/vc-color-picker/components/ColorBlock.tsx @@ -0,0 +1,32 @@ +import type { CSSProperties } from 'vue'; +import { defineComponent } from 'vue'; + +import classNames from '../../_util/classNames'; + +export type ColorBlockProps = { + color: string; + prefixCls?: string; + className?: string; + style?: CSSProperties; + onClick?: (e: MouseEvent) => void; +}; + +const ColorBlock = defineComponent({ + name: 'ColorBlock', + props: ['color', 'prefixCls', 'onClick'], + setup(props: ColorBlockProps) { + const colorBlockCls = `${props.prefixCls}-color-block`; + return () => ( +
+
+
+ ); + }, +}); + +export default ColorBlock; diff --git a/components/vc-color-picker/components/Gradient.tsx b/components/vc-color-picker/components/Gradient.tsx new file mode 100644 index 0000000000..f0a79bcfd2 --- /dev/null +++ b/components/vc-color-picker/components/Gradient.tsx @@ -0,0 +1,46 @@ +import { computed, defineComponent } from 'vue'; + +import type { Color } from '../color'; +import type { HsbaColorType } from '../interface'; +import { generateColor } from '../util'; + +interface ColorBlockProps { + colors: (Color | string)[]; + direction?: string; + type?: HsbaColorType; + prefixCls?: string; +} + +const Gradient = defineComponent({ + name: 'ColorBlock', + props: ['direction', 'prefixCls', 'type', 'colors'], + setup(props: ColorBlockProps, { slots }) { + const direction = computed(() => props.direction || 'to right'); + const gradientColors = computed(() => { + const { colors, type } = props; + return colors + .map((color, idx) => { + const result = generateColor(color); + if (type === 'alpha' && idx === colors.length - 1) { + result.setAlpha(1); + } + return result.toRgbString(); + }) + .join(','); + }); + return () => ( +
+ {slots.children?.()} +
+ ); + }, +}); + +export default Gradient; diff --git a/components/vc-color-picker/components/Handler.tsx b/components/vc-color-picker/components/Handler.tsx new file mode 100644 index 0000000000..14b46e014e --- /dev/null +++ b/components/vc-color-picker/components/Handler.tsx @@ -0,0 +1,29 @@ +import classNames from '../../_util/classNames'; +import { computed, defineComponent } from 'vue'; +type HandlerSize = 'default' | 'small'; + +interface HandlerProps { + size?: HandlerSize; + color?: string; + prefixCls?: string; +} + +const Handler = defineComponent({ + name: 'Handler', + props: ['size', 'color', 'prefixCls'], + setup(props: HandlerProps) { + const size = computed(() => props.size || 'default'); + return () => ( +
+ ); + }, +}); + +export default Handler; diff --git a/components/vc-color-picker/components/Palette.tsx b/components/vc-color-picker/components/Palette.tsx new file mode 100644 index 0000000000..da4833263a --- /dev/null +++ b/components/vc-color-picker/components/Palette.tsx @@ -0,0 +1,20 @@ +import { defineComponent } from 'vue'; + +const Palette = defineComponent({ + name: 'Palette', + props: ['prefixCls'], + setup(props, { slots }) { + return () => ( +
+ {slots.default?.()} +
+ ); + }, +}); + +export default Palette; diff --git a/components/vc-color-picker/components/Picker.tsx b/components/vc-color-picker/components/Picker.tsx new file mode 100644 index 0000000000..051a9fd941 --- /dev/null +++ b/components/vc-color-picker/components/Picker.tsx @@ -0,0 +1,62 @@ +import { defineComponent, ref, shallowRef } from 'vue'; +import useColorDrag from '../hooks/useColorDrag'; +import type { BaseColorPickerProps } from '../interface'; +import { calculateColor, calculateOffset } from '../util'; + +import Handler from './Handler'; +import Palette from './Palette'; +import Transform from './Transform'; + +export type PickerProps = BaseColorPickerProps; + +const Picker = defineComponent({ + name: 'Picker', + props: ['color', 'prefixCls', 'onChange', 'onChangeComplete', 'disabled'], + setup(props: PickerProps) { + const pickerRef = shallowRef(); + const transformRef = shallowRef(); + const colorRef = ref(props.color); + const [offset, dragStartHandle] = useColorDrag({ + color: props.color, + containerRef: pickerRef, + targetRef: transformRef, + calculate: containerRef => calculateOffset(containerRef, transformRef, props.color), + onDragChange: offsetValue => { + const calcColor = calculateColor({ + offset: offsetValue, + targetRef: transformRef, + containerRef: pickerRef, + color: props.color, + }); + colorRef.value = calcColor; + props?.onChange(calcColor); + }, + onDragChangeComplete: () => props?.onChangeComplete?.(colorRef.value), + disabledDrag: props.disabled, + }); + return () => ( +
+ + + + +
+ +
+ ); + }, +}); + +export default Picker; diff --git a/components/vc-color-picker/components/Slider.tsx b/components/vc-color-picker/components/Slider.tsx new file mode 100644 index 0000000000..e1c842a5f4 --- /dev/null +++ b/components/vc-color-picker/components/Slider.tsx @@ -0,0 +1,84 @@ +import { computed, defineComponent, ref, shallowRef } from 'vue'; + +import useColorDrag from '../hooks/useColorDrag'; +import type { BaseColorPickerProps, HsbaColorType } from '../interface'; +import { calculateColor, calculateOffset } from '../util'; +import Palette from './Palette'; + +import Gradient from './Gradient'; +import Handler from './Handler'; +import Transform from './Transform'; +import classNames from 'ant-design-vue/es/_util/classNames'; + +interface SliderProps extends BaseColorPickerProps { + gradientColors: string[]; + direction?: string; + type?: HsbaColorType; + value?: string; +} +const Slider = defineComponent({ + name: 'Slider', + props: [ + 'gradientColors', + 'direction', + 'type', + 'value', + 'color', + 'onChange', + 'onChangeComplete', + 'disabled', + 'prefixCls', + ], + setup(props: SliderProps) { + const sliderRef = shallowRef(); + const transformRef = shallowRef(); + const colorRef = ref(props.color); + const type = computed(() => props.type || 'hue'); + const [offset, dragStartHandle] = useColorDrag({ + color: props.color, + targetRef: transformRef, + containerRef: sliderRef, + calculate: containerRef => + calculateOffset(containerRef, transformRef, props.color, type.value), + onDragChange: offsetValue => { + const calcColor = calculateColor({ + offset: offsetValue, + targetRef: transformRef, + containerRef: sliderRef, + color: props.color, + type: type.value, + }); + colorRef.value = calcColor; + props?.onChange(calcColor); + }, + onDragChangeComplete() { + props.onChangeComplete?.(colorRef.value, type.value); + }, + direction: 'x', + disabledDrag: props.disabled, + }); + + return () => ( +
+ + + + + + +
+ ); + }, +}); + +export default Slider; diff --git a/components/vc-color-picker/components/Transform.tsx b/components/vc-color-picker/components/Transform.tsx new file mode 100644 index 0000000000..d6a049b0b3 --- /dev/null +++ b/components/vc-color-picker/components/Transform.tsx @@ -0,0 +1,33 @@ +import { defineComponent, shallowRef } from 'vue'; +import type { TransformOffset } from '../interface'; +const Transform = defineComponent({ + name: 'Transform', + props: ['offset'], + setup( + props: { + offset: TransformOffset; + }, + { slots, expose }, + ) { + const transformRef = shallowRef(); + expose({ + getRef: () => { + return transformRef.value; + }, + }); + return () => ( +
+ {slots.default?.()} +
+ ); + }, +}); +export default Transform; diff --git a/components/vc-color-picker/hooks/useColorDrag.ts b/components/vc-color-picker/hooks/useColorDrag.ts new file mode 100644 index 0000000000..12343e7fdd --- /dev/null +++ b/components/vc-color-picker/hooks/useColorDrag.ts @@ -0,0 +1,134 @@ +// import { useEffect, useRef, useState } from 'react'; +import { ref, shallowRef, type Ref, watch, onUnmounted } from 'vue'; +import type { Color } from '../color'; +import type { TransformOffset } from '../interface'; + +type EventType = MouseEvent | TouchEvent; + +type EventHandle = (e: EventType) => void; + +interface useColorDragProps { + color?: Color; + offset?: TransformOffset; + containerRef: Ref; + targetRef: Ref; + direction?: 'x' | 'y'; + onDragChange?: (offset: TransformOffset) => void; + onDragChangeComplete?: () => void; + calculate?: (containerRef: Ref) => TransformOffset; + /** Disabled drag */ + disabledDrag?: boolean; +} + +function getPosition(e: EventType) { + const obj = 'touches' in e ? e.touches[0] : e; + const scrollXOffset = + document.documentElement.scrollLeft || document.body.scrollLeft || window.pageXOffset; + const scrollYOffset = + document.documentElement.scrollTop || document.body.scrollTop || window.pageYOffset; + return { pageX: obj.pageX - scrollXOffset, pageY: obj.pageY - scrollYOffset }; +} + +function useColorDrag(props: useColorDragProps): [Ref, EventHandle] { + const { + offset, + targetRef, + containerRef, + direction, + onDragChange, + onDragChangeComplete, + calculate, + color, + disabledDrag, + } = props; + const offsetValue = ref(offset || { x: 0, y: 0 }); + const mouseMoveRef = shallowRef<(event: MouseEvent) => void>(null); + const mouseUpRef = shallowRef<(event: MouseEvent) => void>(null); + const dragRef = shallowRef({ + flag: false, + }); + + watch( + () => [color, containerRef], + () => { + if (dragRef.value.flag === false) { + const calcOffset = calculate?.(containerRef); + if (calcOffset) { + offsetValue.value = calcOffset; + } + } + }, + { + deep: true, + }, + ); + onUnmounted(() => { + document.removeEventListener('mousemove', mouseMoveRef.value); + document.removeEventListener('mouseup', mouseUpRef.value); + document.removeEventListener('touchmove', mouseMoveRef.value); + document.removeEventListener('touchend', mouseUpRef.value); + mouseMoveRef.value = null; + mouseUpRef.value = null; + }); + + const updateOffset: EventHandle = e => { + const { pageX, pageY } = getPosition(e); + const { x: rectX, y: rectY, width, height } = containerRef.value.getBoundingClientRect(); + const { width: targetWidth, height: targetHeight } = targetRef.value + // @ts-ignore + .getRef() + .getBoundingClientRect(); + + const centerOffsetX = targetWidth / 2; + const centerOffsetY = targetHeight / 2; + + const offsetX = Math.max(0, Math.min(pageX - rectX, width)) - centerOffsetX; + const offsetY = Math.max(0, Math.min(pageY - rectY, height)) - centerOffsetY; + + const calcOffset = { + x: offsetX, + y: direction === 'x' ? offsetValue.value.y : offsetY, + }; + + // Exclusion of boundary cases + if ((targetWidth === 0 && targetHeight === 0) || targetWidth !== targetHeight) { + return false; + } + offsetValue.value = calcOffset; + onDragChange?.(calcOffset); + }; + + const onDragMove: EventHandle = e => { + e.preventDefault(); + updateOffset(e); + }; + + const onDragStop: EventHandle = e => { + e.preventDefault(); + dragRef.value.flag = false; + document.removeEventListener('mousemove', mouseMoveRef.value); + document.removeEventListener('mouseup', mouseUpRef.value); + document.removeEventListener('touchmove', mouseMoveRef.value); + document.removeEventListener('touchend', mouseUpRef.value); + mouseMoveRef.value = null; + mouseUpRef.value = null; + onDragChangeComplete?.(); + }; + + const onDragStart: EventHandle = e => { + if (disabledDrag) { + return; + } + updateOffset(e); + dragRef.value.flag = true; + document.addEventListener('mousemove', onDragMove); + document.addEventListener('mouseup', onDragStop); + document.addEventListener('touchmove', onDragMove); + document.addEventListener('touchend', onDragStop); + mouseMoveRef.value = onDragMove; + mouseUpRef.value = onDragStop; + }; + return [offsetValue, onDragStart]; +} + +export default useColorDrag; diff --git a/components/vc-color-picker/hooks/useColorState.ts b/components/vc-color-picker/hooks/useColorState.ts new file mode 100644 index 0000000000..3f855e566e --- /dev/null +++ b/components/vc-color-picker/hooks/useColorState.ts @@ -0,0 +1,40 @@ +import type { Ref } from 'vue'; +import { watch } from 'vue'; +import useState from '../../_util/hooks/useState'; +import type { ColorGenInput } from '../interface'; +import { generateColor } from '../util'; + +type ColorValue = ColorGenInput | undefined; + +function hasValue(value: ColorValue) { + return value !== undefined; +} + +const useColorState = ( + defaultStateValue: ColorValue, + option: { + defaultValue?: ColorValue; + value?: Ref; + }, +) => { + const { defaultValue, value: color } = option; + const [colorValue, setColorValue] = useState(() => { + let mergeState; + if (hasValue(color.value)) { + mergeState = color.value; + } else if (hasValue(defaultValue)) { + mergeState = defaultValue; + } else { + mergeState = defaultStateValue; + } + return generateColor(mergeState); + }); + + watch(option.value, value => { + setColorValue(generateColor(value)); + }); + + return [colorValue, setColorValue] as const; +}; + +export default useColorState; diff --git a/components/vc-color-picker/index.tsx b/components/vc-color-picker/index.tsx new file mode 100644 index 0000000000..3d87d7d6df --- /dev/null +++ b/components/vc-color-picker/index.tsx @@ -0,0 +1,8 @@ +import ColorPicker from './ColorPicker'; +export { Color } from './color'; +export type { ColorPickerProps } from './ColorPicker'; +export { default as ColorBlock } from './components/ColorBlock'; +export type { ColorBlockProps } from './components/ColorBlock'; +export * from './interface'; + +export default ColorPicker; diff --git a/components/vc-color-picker/interface.ts b/components/vc-color-picker/interface.ts new file mode 100644 index 0000000000..941a7580b3 --- /dev/null +++ b/components/vc-color-picker/interface.ts @@ -0,0 +1,38 @@ +import type { Color } from './color'; + +export interface HSB { + h: number | string; + s: number | string; + b: number | string; +} + +export interface RGB { + r: number | string; + g: number | string; + b: number | string; +} + +export interface HSBA extends HSB { + a: number; +} + +export interface RGBA extends RGB { + a: number; +} + +export type ColorGenInput = string | number | RGB | RGBA | HSB | HSBA | T; + +export type HsbaColorType = 'hue' | 'alpha'; + +export type TransformOffset = { + x: number; + y: number; +}; + +export interface BaseColorPickerProps { + color?: Color; + prefixCls?: string; + disabled?: boolean; + onChange?: (color: Color, type?: HsbaColorType) => void; + onChangeComplete?: (value: Color, type?: HsbaColorType) => void; +} diff --git a/components/vc-color-picker/util.ts b/components/vc-color-picker/util.ts new file mode 100644 index 0000000000..99e927a588 --- /dev/null +++ b/components/vc-color-picker/util.ts @@ -0,0 +1,99 @@ +import type { Ref } from 'vue'; +import { Color } from './color'; +import type { ColorGenInput, HsbaColorType, TransformOffset } from './interface'; + +export const ColorPickerPrefixCls = 'rc-color-picker'; + +export const generateColor = (color: ColorGenInput): Color => { + if (color instanceof Color) { + return color; + } + return new Color(color); +}; + +export const defaultColor = generateColor('#1677ff'); + +export const calculateColor = (props: { + offset: TransformOffset; + containerRef: Ref; + targetRef: Ref; + color?: Color; + type?: HsbaColorType; +}): Color => { + const { offset, targetRef, containerRef, color, type } = props; + const { width, height } = containerRef.value.getBoundingClientRect(); + const { width: targetWidth, height: targetHeight } = targetRef.value + // @ts-ignore + .getRef() + .getBoundingClientRect(); + + const centerOffsetX = targetWidth / 2; + const centerOffsetY = targetHeight / 2; + const saturation = (offset.x + centerOffsetX) / width; + const bright = 1 - (offset.y + centerOffsetY) / height; + const hsb = color.toHsb(); + const alphaOffset = saturation; + const hueOffset = ((offset.x + centerOffsetX) / width) * 360; + + if (type) { + switch (type) { + case 'hue': + return generateColor({ + ...hsb, + h: hueOffset <= 0 ? 0 : hueOffset, + }); + case 'alpha': + return generateColor({ + ...hsb, + a: alphaOffset <= 0 ? 0 : alphaOffset, + }); + } + } + + return generateColor({ + h: hsb.h, + s: saturation <= 0 ? 0 : saturation, + b: bright >= 1 ? 1 : bright, + a: hsb.a, + }); +}; + +export const calculateOffset = ( + containerRef: Ref, + targetRef: Ref, + color?: Color, + type?: HsbaColorType, +): TransformOffset => { + const { width, height } = containerRef.value.getBoundingClientRect(); + const { width: targetWidth, height: targetHeight } = targetRef.value + // @ts-ignore + .getRef() + .getBoundingClientRect(); + const centerOffsetX = targetWidth / 2; + const centerOffsetY = targetHeight / 2; + const hsb = color.toHsb(); + + // Exclusion of boundary cases + if ((targetWidth === 0 && targetHeight === 0) || targetWidth !== targetHeight) { + return; + } + + if (type) { + switch (type) { + case 'hue': + return { + x: (hsb.h / 360) * width - centerOffsetX, + y: -centerOffsetY / 3, + }; + case 'alpha': + return { + x: (hsb.a / 1) * width - centerOffsetX, + y: -centerOffsetY / 3, + }; + } + } + return { + x: hsb.s * width - centerOffsetX, + y: (1 - hsb.b) * height - centerOffsetY, + }; +}; From 141348c4e9b7f476e2b2da9bb645fc2222f545ad Mon Sep 17 00:00:00 2001 From: CCherry07 <2405693142@qq.com> Date: Mon, 10 Jul 2023 00:42:59 +0800 Subject: [PATCH 02/23] feat: color-picker --- components/color-picker/ColorPicker.tsx | 201 ++++++++++++++++++ components/color-picker/ColorPickerPanel.tsx | 67 ++++++ components/color-picker/color.ts | 44 ++++ .../components/ColorAlphaInput.tsx | 52 +++++ .../color-picker/components/ColorClear.tsx | 25 +++ .../color-picker/components/ColorHexInput.tsx | 51 +++++ .../color-picker/components/ColorHsbInput.tsx | 74 +++++++ .../color-picker/components/ColorInput.tsx | 75 +++++++ .../color-picker/components/ColorPresets.tsx | 84 ++++++++ .../color-picker/components/ColorRgbInput.tsx | 70 ++++++ .../color-picker/components/ColorSteppers.tsx | 51 +++++ .../color-picker/components/ColorTrigger.tsx | 66 ++++++ components/color-picker/demo/allowClear.vue | 21 ++ components/color-picker/demo/base.vue | 24 +++ components/color-picker/demo/index.vue | 26 +++ .../color-picker/hooks/useColorState.ts | 37 ++++ components/color-picker/index.en-US.md | 69 ++++++ components/color-picker/index.ts | 7 + components/color-picker/index.tsx | 1 - components/color-picker/index.zh-CN.md | 70 ++++++ components/color-picker/interface.ts | 33 +++ components/color-picker/style/color-block.ts | 35 +++ components/color-picker/style/index.ts | 154 ++++++++++++++ components/color-picker/style/input.ts | 100 +++++++++ components/color-picker/style/picker.ts | 77 +++++++ components/color-picker/style/presets.ts | 113 ++++++++++ components/color-picker/util.ts | 20 ++ components/components.ts | 3 + components/theme/interface/components.ts | 2 + components/vc-color-picker/ColorPicker.tsx | 13 +- .../vc-color-picker/components/ColorBlock.tsx | 10 +- .../vc-color-picker/components/Picker.tsx | 9 +- .../vc-color-picker/components/Slider.tsx | 7 +- .../vc-color-picker/hooks/useColorDrag.ts | 25 ++- .../vc-color-picker/hooks/useColorState.ts | 2 - site/src/demo.js | 12 +- 36 files changed, 1700 insertions(+), 30 deletions(-) create mode 100644 components/color-picker/ColorPicker.tsx create mode 100644 components/color-picker/ColorPickerPanel.tsx create mode 100644 components/color-picker/color.ts create mode 100644 components/color-picker/components/ColorAlphaInput.tsx create mode 100644 components/color-picker/components/ColorClear.tsx create mode 100644 components/color-picker/components/ColorHexInput.tsx create mode 100644 components/color-picker/components/ColorHsbInput.tsx create mode 100644 components/color-picker/components/ColorInput.tsx create mode 100644 components/color-picker/components/ColorPresets.tsx create mode 100644 components/color-picker/components/ColorRgbInput.tsx create mode 100644 components/color-picker/components/ColorSteppers.tsx create mode 100644 components/color-picker/components/ColorTrigger.tsx create mode 100644 components/color-picker/demo/allowClear.vue create mode 100644 components/color-picker/demo/base.vue create mode 100644 components/color-picker/demo/index.vue create mode 100644 components/color-picker/hooks/useColorState.ts create mode 100644 components/color-picker/index.en-US.md create mode 100644 components/color-picker/index.ts delete mode 100644 components/color-picker/index.tsx create mode 100644 components/color-picker/index.zh-CN.md create mode 100644 components/color-picker/interface.ts create mode 100644 components/color-picker/style/color-block.ts create mode 100644 components/color-picker/style/index.ts create mode 100644 components/color-picker/style/input.ts create mode 100644 components/color-picker/style/picker.ts create mode 100644 components/color-picker/style/presets.ts create mode 100644 components/color-picker/util.ts diff --git a/components/color-picker/ColorPicker.tsx b/components/color-picker/ColorPicker.tsx new file mode 100644 index 0000000000..4256512f4f --- /dev/null +++ b/components/color-picker/ColorPicker.tsx @@ -0,0 +1,201 @@ +import { computed, defineComponent, shallowRef, watch } from 'vue'; +import PropTypes from '../_util/vue-types'; +import type { HsbaColorType } from '../vc-color-picker'; +import type { PopoverProps } from '../popover'; +import Popover from '../popover'; +import theme from '../theme'; +import ColorPickerPanel from './ColorPickerPanel'; +import type { Color } from './color'; +import ColorTrigger from './components/ColorTrigger'; +import useColorState from './hooks/useColorState'; +import type { + ColorFormat, + ColorPickerBaseProps, + PresetsItem, + TriggerPlacement, + TriggerType, +} from './interface'; +import useStyle from './style/index'; +import { generateColor } from './util'; +import type { CSSProperties, ComputedRef, ExtractPropTypes, PropType } from 'vue'; +import useConfigInject from '../config-provider/hooks/useConfigInject'; +import useMergedState from '../_util/hooks/useMergedState'; +import classNames from '../_util/classNames'; + +const colorPickerProps = () => ({ + value: { + type: [String, Object] as PropType, + default: undefined, + }, + defaultValue: { + type: [String, Object] as PropType, + default: undefined, + }, + open: PropTypes.looseBool, + disabled: PropTypes.looseBool, + placement: { + type: String as PropType, + default: 'bottomLeft', + }, + trigger: { + type: String as PropType, + default: 'click', + }, + format: { + type: String as PropType, + default: 'hex', + }, + allowClear: { + type: Boolean as PropType, + default: false, + }, + presets: { + type: Array as PropType, + default: () => [], + }, + arrow: { + type: [Boolean, Object] as PropType, + default: true, + }, + styles: { + type: Object as PropType<{ popup?: CSSProperties }>, + default: () => ({}), + }, + rootClassName: PropTypes.string, + onOpenChange: { + type: Function as PropType<(open: boolean) => void>, + default: () => {}, + }, + onFormatChange: { + type: Function as PropType<(format: ColorFormat) => void>, + default: () => {}, + }, + onChange: { + type: Function as PropType<(value: Color, hex: string) => void>, + default: () => {}, + }, + getPopupContainer: { + type: Function as PropType, + default: undefined, + }, + autoAdjustOverflow: { + type: Boolean as PropType, + default: true, + }, +}); + +export type ColorPickerProps = Partial>>; + +const ColorPicker = defineComponent({ + name: 'AColorPicker', + inheritAttrs: false, + props: colorPickerProps(), + setup(props, { slots, attrs, emit }) { + const { prefixCls, getPopupContainer, direction } = useConfigInject('color-picker', props); + const { token } = theme.useToken(); + const value = computed(() => props.value); + const [colorValue, setColorValue] = useColorState(token.value.colorPrimary, { + value, + defaultValue: props.defaultValue, + }); + const open = computed(() => props.open); + const [popupOpen, setPopupOpen] = useMergedState(false, { + value: open, + postState: openData => !props.disabled && openData, + onChange: props.onOpenChange, + }); + + const colorCleared = shallowRef(false); + + const [wrapSSR, hashId] = useStyle(prefixCls); + + const handleChange = (data: Color, type?: HsbaColorType) => { + let color: Color = generateColor(data); + if (colorCleared.value) { + colorCleared.value = false; + const hsba = color.toHsb(); + // ignore alpha slider + if (colorValue.value.toHsb().a === 0 && type !== 'alpha') { + hsba.a = 1; + color = generateColor(hsba); + } + } + if (!props.value) { + setColorValue(color); + } + emit('update:value', color, color.toHexString()); + emit('change', color, color.toHexString()); + }; + + const handleClear = (clear: boolean) => { + colorCleared.value = clear; + }; + + const popoverProps: ComputedRef = computed(() => ({ + open: popupOpen.value, + trigger: props.trigger, + placement: props.placement, + arrow: props.arrow, + rootClassName: props.rootClassName, + getPopupContainer: getPopupContainer.value, + autoAdjustOverflow: props.autoAdjustOverflow, + })); + const colorBaseProps: ComputedRef = computed(() => ({ + prefixCls: prefixCls.value, + color: colorValue.value, + allowClear: props.allowClear, + colorCleared: colorCleared.value, + disabled: props.disabled, + presets: props.presets, + format: props.format, + onFormatChange: props.onFormatChange, + })); + watch( + colorCleared, + () => { + if (colorCleared.value) { + setPopupOpen(false); + } + }, + { + immediate: true, + }, + ); + return () => { + const mergeRootCls = classNames(props.rootClassName, { + [`${prefixCls.value}-rtl`]: direction, + }); + const mergeCls = classNames(mergeRootCls, hashId.value); + return wrapSSR( + + } + overlayClassName={prefixCls.value} + {...popoverProps.value} + > + {slots.children?.() || ( + + )} + , + ); + }; + }, +}); + +export default ColorPicker; diff --git a/components/color-picker/ColorPickerPanel.tsx b/components/color-picker/ColorPickerPanel.tsx new file mode 100644 index 0000000000..7d02539a31 --- /dev/null +++ b/components/color-picker/ColorPickerPanel.tsx @@ -0,0 +1,67 @@ +import type { HsbaColorType } from '../vc-color-picker'; +import { computed, defineComponent } from 'vue'; +import VcColorPicker from '../vc-color-picker'; +import Divider from '../divider'; +import type { Color } from './color'; +import ColorClear from './components/ColorClear'; +import ColorInput from './components/ColorInput'; +import ColorPresets from './components/ColorPresets'; +import type { ColorPickerBaseProps } from './interface'; +import type { VueNode } from '../_util/type'; + +interface ColorPickerPanelProps extends ColorPickerBaseProps { + onChange?: (value?: Color, type?: HsbaColorType) => void; + onClear?: (clear?: boolean) => void; +} +const ColorPickerPanel = defineComponent({ + name: 'ColorPickerPanel', + inheritAttrs: false, + props: ['prefixCls', 'allowClear', 'presets', 'onChange', 'onClear', 'color'], + setup(props: ColorPickerPanelProps, { attrs }) { + const colorPickerPanelPrefixCls = computed(() => `${props.prefixCls}-inner-panel`); + const newLocal = (panel: VueNode) => ( +
+ {props.allowClear && ( + { + props.onChange?.(clearColor); + props.onClear?.(true); + }} + {...attrs} + /> + )} + {panel} + + {Array.isArray(props.presets) && ( + <> + + + + )} +
+ ); + const extraPanelRender = newLocal; + return () => ( + + ); + }, +}); + +export default ColorPickerPanel; diff --git a/components/color-picker/color.ts b/components/color-picker/color.ts new file mode 100644 index 0000000000..1a478d3073 --- /dev/null +++ b/components/color-picker/color.ts @@ -0,0 +1,44 @@ +/* eslint-disable class-methods-use-this */ +import { getHex } from './util'; +import type { ColorGenInput } from '../vc-color-picker'; +import { Color as RcColor } from '../vc-color-picker'; + +export type Color = Pick< + RcColor, + 'toHsb' | 'toHsbString' | 'toHex' | 'toHexString' | 'toRgb' | 'toRgbString' +>; + +export class ColorFactory { + /** Original Color object */ + private metaColor: RcColor; + + constructor(color: ColorGenInput) { + this.metaColor = new RcColor(color as ColorGenInput); + } + + toHsb() { + return this.metaColor.toHsb(); + } + + toHsbString() { + return this.metaColor.toHsbString(); + } + + toHex() { + return getHex(this.toHexString(), this.metaColor.getAlpha() < 1); + } + + toHexString() { + return this.metaColor.getAlpha() === 1 + ? this.metaColor.toHexString() + : this.metaColor.toHex8String(); + } + + toRgb() { + return this.metaColor.toRgb(); + } + + toRgbString() { + return this.metaColor.toRgbString(); + } +} diff --git a/components/color-picker/components/ColorAlphaInput.tsx b/components/color-picker/components/ColorAlphaInput.tsx new file mode 100644 index 0000000000..c5e6960eea --- /dev/null +++ b/components/color-picker/components/ColorAlphaInput.tsx @@ -0,0 +1,52 @@ +import { computed, defineComponent, ref, watch } from 'vue'; +import type { Color } from '../color'; +import type { ColorPickerBaseProps } from '../interface'; +import { generateColor, getAlphaColor } from '../util'; +import ColorSteppers from './ColorSteppers'; + +interface ColorAlphaInputProps extends Pick { + value?: Color; + onChange?: (value: Color) => void; +} +const ColorAlphaInput = defineComponent({ + name: 'ColorAlphaInput', + props: ['prefixCls', 'value', 'onChange'], + setup(props: ColorAlphaInputProps) { + const colorAlphaInputPrefixCls = computed(() => `${props.prefixCls}-alpha-input`); + const alphaValue = ref(generateColor(props.value || '#000')); + watch( + () => props.value, + val => { + if (val) { + alphaValue.value = generateColor(val); + } + }, + { + immediate: true, + }, + ); + const handleAlphaChange = (step: number) => { + const hsba = alphaValue.value.toHsb(); + hsba.a = (step || 0) / 100; + const genColor = generateColor(hsba); + if (!props.value) { + alphaValue.value = genColor; + } + props.onChange?.(genColor); + }; + + return () => ( + `${step}%`} + class={colorAlphaInputPrefixCls.value} + onChange={handleAlphaChange} + min={0} + max={100} + /> + ); + }, +}); + +export default ColorAlphaInput; diff --git a/components/color-picker/components/ColorClear.tsx b/components/color-picker/components/ColorClear.tsx new file mode 100644 index 0000000000..8dad6b9e19 --- /dev/null +++ b/components/color-picker/components/ColorClear.tsx @@ -0,0 +1,25 @@ +import { defineComponent } from 'vue'; +import type { Color } from '../color'; +import type { ColorPickerBaseProps } from '../interface'; +import { generateColor } from '../util'; + +interface ColorClearProps extends Pick { + value?: Color; + onChange?: (value: Color) => void; +} +const ColorClear = defineComponent({ + name: 'ColorClear', + props: ['prefixCls', 'value', 'onChange'], + setup(props: ColorClearProps) { + const handleClick = () => { + if (props.value) { + const hsba = props.value.toHsb(); + hsba.a = 0; + const genColor = generateColor(hsba); + props.onChange?.(genColor); + } + }; + return () =>
; + }, +}); +export default ColorClear; diff --git a/components/color-picker/components/ColorHexInput.tsx b/components/color-picker/components/ColorHexInput.tsx new file mode 100644 index 0000000000..0aadce6a38 --- /dev/null +++ b/components/color-picker/components/ColorHexInput.tsx @@ -0,0 +1,51 @@ +import { computed, defineComponent, ref, watch } from 'vue'; +import Input from '../../input'; +import type { Color } from '../color'; +import type { ColorPickerBaseProps } from '../interface'; +import { generateColor, toHexFormat } from '../util'; + +interface ColorHexInputProps extends Pick { + value?: Color; + onChange?: (value: Color) => void; +} + +const hexReg = /(^#[\da-f]{6}$)|(^#[\da-f]{8}$)/i; +const isHexString = (hex?: string) => hexReg.test(`#${hex}`); +const ColorHexInput = defineComponent({ + name: 'ColorHexInput', + props: ['prefixCls', 'value', 'onChange'], + setup(props: ColorHexInputProps) { + const colorHexInputPrefixCls = computed(() => `${props.prefixCls}-hex-input`); + const hexValue = ref(props.value?.toHex()); + watch( + () => props.value, + val => { + const hex = val?.toHex(); + if (isHexString(hex) && val) { + hexValue.value = toHexFormat(hex); + } + }, + { + immediate: true, + }, + ); + const handleHexChange = e => { + const originValue = e.target.value; + hexValue.value = toHexFormat(originValue); + if (isHexString(toHexFormat(originValue, true))) { + props.onChange?.(generateColor(originValue)); + } + }; + return () => ( + + ); + }, +}); + +export default ColorHexInput; diff --git a/components/color-picker/components/ColorHsbInput.tsx b/components/color-picker/components/ColorHsbInput.tsx new file mode 100644 index 0000000000..f04ae74e00 --- /dev/null +++ b/components/color-picker/components/ColorHsbInput.tsx @@ -0,0 +1,74 @@ +import { computed, defineComponent, ref, watch } from 'vue'; + +import { getRoundNumber } from '../../vc-color-picker/color'; +import type { HSB } from '../../vc-color-picker'; +import type { Color } from '../color'; +import type { ColorPickerBaseProps } from '../interface'; +import { generateColor } from '../util'; +import ColorSteppers from './ColorSteppers'; + +interface ColorHsbInputProps extends Pick { + value?: Color; + onChange?: (value: Color) => void; +} +const ColorHsbInput = defineComponent({ + name: 'ColorHsbInput', + props: ['prefixCls', 'value', 'onChange'], + setup(props: ColorHsbInputProps) { + const colorHsbInputPrefixCls = computed(() => `${props.prefixCls}-hsb-input`); + const hsbValue = ref(generateColor(props.value || '#000')); + watch( + () => props.value, + val => { + if (val) { + hsbValue.value = generateColor(val || '#000'); + } + }, + { + immediate: true, + }, + ); + const handleHsbChange = (step: number, type: keyof HSB) => { + const hsb = hsbValue.value.toHsb(); + hsb[type] = type === 'h' ? step : (step || 0) / 100; + const genColor = generateColor(hsb); + if (!props.value) { + hsbValue.value = genColor; + } + props.onChange?.(genColor); + }; + return () => ( +
+ getRoundNumber(step || 0).toString()} + onChange={step => handleHsbChange(Number(step), 'h')} + /> + `${getRoundNumber(step || 0)}%`} + onChange={step => handleHsbChange(Number(step), 's')} + /> + `${getRoundNumber(step || 0)}%`} + onChange={step => handleHsbChange(Number(step), 'b')} + /> +
+ ); + }, +}); + +export default ColorHsbInput; diff --git a/components/color-picker/components/ColorInput.tsx b/components/color-picker/components/ColorInput.tsx new file mode 100644 index 0000000000..163c163b7e --- /dev/null +++ b/components/color-picker/components/ColorInput.tsx @@ -0,0 +1,75 @@ +import type { VNode } from 'vue'; +import { computed, defineComponent } from 'vue'; +import Select from '../../select'; +import type { Color } from '../color'; +import type { ColorPickerBaseProps } from '../interface'; +import { ColorFormat } from '../interface'; +import ColorAlphaInput from './ColorAlphaInput'; +import ColorHexInput from './ColorHexInput'; +import ColorHsbInput from './ColorHsbInput'; +import ColorRgbInput from './ColorRgbInput'; +import useMergedState from '../../_util/hooks/useMergedState'; + +interface ColorInputProps + extends Pick { + value?: Color; + onChange?: (value: Color) => void; +} + +const selectOptions = [ColorFormat.hex, ColorFormat.hsb, ColorFormat.rgb].map(format => ({ + value: format, + label: format.toLocaleUpperCase(), +})); + +const ColorInput = defineComponent({ + name: 'ColorInput', + props: ['prefixCls', 'format', 'onFormatChange', 'value', 'onChange'], + setup(props: ColorInputProps) { + const [colorFormat, setColorFormat] = useMergedState(props.format, { + onChange: props.onFormatChange, + }); + + const colorInputPrefixCls = computed(() => `${props.prefixCls}-input`); + const handleFormatChange = (newFormat: ColorFormat) => { + setColorFormat(newFormat); + }; + const steppersNode = computed(() => { + const inputProps = { + value: props.value, + prefixCls: props.prefixCls, + onChange: props.onChange, + }; + switch (colorFormat.value) { + case ColorFormat.hsb: + return ; + case ColorFormat.rgb: + return ; + case ColorFormat.hex: + default: + return ; + } + }); + return () => ( +
+