React 通关秘籍 组件实战:ColorPicker(二)

React 通关秘籍 - zxg_神说要有光 - 掘金小册

这节我们开始写 ColorPicker 组件。

看下 antd 的 ColorPicker 组件

可以分成这三部分:

上面是一个 ColorPickerPanel,可以通过滑块选择颜色,调整色相、饱和度。

下面是 ColorInput,可以通过输入框修改颜色,可以切换 RGB、HEX 等色彩模式。

外面是一个 Popover 气泡卡片。

我们先写 ColorPickerPanel 的部分:

这部分分为上面的调色板 Palette,下面的 Slider 滑动条。

这两个组件写起来就比较简单了。

这样一拆解,是不是思路就清晰了呢?

新建个项目:

 

css

复制代码

npx create-react-app --template=typescript color-picker-component

新建 ColorPicker 目录,然后创建 ColorPickerPanel 组件:

 

javascript

复制代码

import { CSSProperties } from "react"; import cs from 'classnames'; import './index.scss'; export interface ColorPickerProps { className?: string; style?: CSSProperties } function ColorPickerPanel(props: ColorPickerProps) { const { className, style } = props; const classNames = cs("color-picker", className); return <div className={classNames} style={style}> </div> } export default ColorPickerPanel;

安装用到的 classnames 包:

 

css

复制代码

npm install --save classnames

style 和 className 这俩 props 就不用解释了。

然后添加 value 和 onChange 的参数:

 

javascript

复制代码

interface ColorPickerProps { className?: string; style?: CSSProperties; value?: string; onChange?: (color: string) => void; }

这里颜色用 string 类型不大好,最好是有专门的 Color 类,可以用来切换 RGB、HSL、HEX 等颜色格式。

直接用 @ctrl/tinycolor 这个包就行。

 

css

复制代码

npm install --save @ctrl/tinycolor

先试一下这个包:

创建 index.js

 

javascript

复制代码

const { TinyColor } = require("@ctrl/tinycolor"); let color = new TinyColor('red'); console.log(color.toHex()); console.log(color.toHsl()); console.log(color.toRgb()); console.log(); color = new TinyColor('#00ff00'); console.log(color.toHex()); console.log(color.toHsl()); console.log(color.toRgb()); console.log(); color = new TinyColor({ r: 0, g: 0, b: 255}); console.log(color.toHex()); console.log(color.toHsl()); console.log(color.toRgb()); console.log();

跑一下:

可以看到,TinyColor 能识别出颜色的格式,并且在 hex、hsl、rgb 之间进行转换。

然后添加 ColorPicker/color.ts

 

javascript

复制代码

import { TinyColor } from '@ctrl/tinycolor'; export class Color extends TinyColor { }

那 value 直接写 Color 类型么?

也不好,这样用起来得 new 一个 Color 对象才行,不方便。

所以我们类型要这样写:

创建 ColorPicker/interface.ts

 

javascript

复制代码

import type { Color } from './color'; export interface HSL { h: number | string; s: number | string; l: number | string; } export interface RGB { r: number | string; g: number | string; b: number | string; } export interface HSLA extends HSL { a: number; } export interface RGBA extends RGB { a: number; } export type ColorType = | string | number | RGB | RGBA | HSL | HSLA | Color;

支持 string 还有 number 还有 rgb、hsl、rgba、hsla 这几种格式,或者直接传一个 Color 对象。

在组件里判断下 value 类型,如果不是 Color,那就创建一个 Color 对象,传入 Palette:

 

javascript

复制代码

import { CSSProperties, useState } from "react"; import cs from 'classnames'; import { ColorType } from "./interface"; import { Color } from "./color"; import Palette from "./Palette"; import './index.scss'; export interface ColorPickerProps { className?: string; style?: CSSProperties; value?: ColorType; onChange?: (color: Color) => void; } function ColorPickerPanel(props: ColorPickerProps) { const { className, style, value, onChange } = props; const [colorValue, setColorValue] = useState<Color>(() => { if (value instanceof Color) { return value; } return new Color(value); }); const classNames = cs("color-picker", className); return <div className={classNames} style={style}> <Palette color={colorValue}></Palette> </div> } export default ColorPickerPanel;

接下来写 Palette 组件:

 

javascript

复制代码

import type { FC } from 'react'; import { Color } from './color'; const Palette: FC<{ color: Color }> = ({ color }) => { return ( <div className="color-picker-panel-palette" > <div className="color-picker-panel-palette-main" style={{ backgroundColor: `hsl(${color.toHsl().h},100%, 50%)`, backgroundImage: 'linear-gradient(0deg, #000, transparent),linear-gradient(90deg, #fff, hsla(0, 0%, 100%, 0))', }} /> </div> ); }; export default Palette;

拿到 color 的 hsl 值中的色相,然后加一个横向和纵向的渐变就好了。

我们写下样式 ColorPicker/index.scss:

 

scss

复制代码

.color-picker { width: 300px; &-panel { &-palette { position: relative; min-height: 160px; &-main { position: absolute; top: 0; bottom: 0; left: 0; right: 0; } } } }

安装用到的包:

 

css

复制代码

npm install --save-dev sass

跑一下:

 

arduino

复制代码

npm run start

调色板出来了。

还要实现上面的滑块,这个封装个组件,因为 Slider 也会用到:

创建 ColorPicker/Handler.tsx:

 

javascript

复制代码

import classNames from 'classnames'; import type { FC } from 'react'; type HandlerSize = 'default' | 'small'; interface HandlerProps { size?: HandlerSize; color?: string; }; const Handler: FC<HandlerProps> = ({ size = 'default', color }) => { return ( <div className={classNames(`color-picker-panel-palette-handler`, { [`color-picker-panel-palette-handler-sm`]: size === 'small', })} style={{ backgroundColor: color, }} /> ); }; export default Handler;

有 size 和 color 两个参数。

size 是 default 和 small 两个取值,因为这俩滑块是不一样大的:

加一下两种滑块的样式:

 

scss

复制代码

&-handler { box-sizing: border-box; width: 16px; height: 16px; border: 2px solid #fff; border-radius: 50%; box-shadow: 0 0 1px 1px rgba(0, 0, 0, 0); } &-handler-sm { width: 12px; height: 12px; }

在 Palette 引入下:

 

javascript

复制代码

<Handler color={color.toRgbString()}/>

刷新下页面,确实是有的:

只是现在看不到。

加一下 zindex 就好了:

但是不建议写在这里。

为什么呢?

因为这里写了 position: absolute 那是不是 Handler 组件也得加上 x、y 的参数。

这样它就不纯粹了,复用性会变差。

所以可以把定位的样式抽离成一个单独的 Transform 组件:

创建 Transform:

 

javascript

复制代码

import React, { forwardRef } from 'react'; export interface TransformOffset { x: number; y: number; }; interface TransformProps { offset?: TransformOffset; children?: React.ReactNode; } const Transform = forwardRef<HTMLDivElement, TransformProps>((props, ref) => { const { children, offset } = props; return ( <div ref={ref} style={{ position: 'absolute', left: offset?.x ?? 0, top: offset?.y ?? 0, zIndex: 1, }} > {children} </div> ); }); export default Transform;

 

javascript

复制代码

import { useRef, type FC } from 'react'; import { Color } from './color'; import Handler from './Handler'; import Transform from './Transform'; const Palette: FC<{ color: Color }> = ({ color }) => { const transformRef = useRef<HTMLDivElement>(null); return ( <div className="color-picker-panel-palette" > <Transform ref={transformRef} offset={{x: 50, y: 50}}> <Handler color={color.toRgbString()}/> </Transform> <div className={`color-picker-panel-palette-main`} style={{ backgroundColor: `hsl(${color.toHsl().h},100%, 50%)`, backgroundImage: 'linear-gradient(0deg, #000, transparent),linear-gradient(90deg, #fff, hsla(0, 0%, 100%, 0))', }} /> </div> ); }; export default Palette;

看下效果:

如果不单独分 Transform 这个组件呢?

那就是把这段样式写在 Hanlder 组件里,然后加上俩参数:

功能是一样的,但是不如拆分出来复用性好。

然后我们加上拖拽功能。

拖拽就是给元素绑定 mousedown、mousemove、mouseup 事件,在 mousemove 的时候改变 x、y。

这部分逻辑比较复杂,我们封装一个自定义 hook 来做。

创建 ColorPicker/useColorDrag.ts

 

javascript

复制代码

import { useEffect, useRef, useState } from 'react'; import { TransformOffset } from './Transform'; type EventType = | MouseEvent | React.MouseEvent<Element, MouseEvent> type EventHandle = (e: EventType) => void; interface useColorDragProps { offset?: TransformOffset; containerRef: React.RefObject<HTMLDivElement>; targetRef: React.RefObject<HTMLDivElement>; direction?: 'x' | 'y'; onDragChange?: (offset: TransformOffset) => void; } function useColorDrag( props: useColorDragProps, ): [TransformOffset, EventHandle] { const { offset, targetRef, containerRef, direction, onDragChange, } = props; const [offsetValue, setOffsetValue] = useState(offset || { x: 0, y: 0 }); const dragRef = useRef({ flag: false }); useEffect(() => { document.removeEventListener('mousemove', onDragMove); document.removeEventListener('mouseup', onDragStop); }, []); const updateOffset: EventHandle = e => { }; const onDragStop: EventHandle = e => { document.removeEventListener('mousemove', onDragMove); document.removeEventListener('mouseup', onDragStop); dragRef.current.flag = false; }; const onDragMove: EventHandle = e => { e.preventDefault(); updateOffset(e); }; const onDragStart: EventHandle = e => { document.addEventListener('mousemove', onDragMove); document.addEventListener('mouseup', onDragStop); dragRef.current.flag = true; }; return [offsetValue, onDragStart]; } export default useColorDrag;

代码比较多,从上到下来看:

MouseEvent 是 ts 内置的鼠标事件类型,而 React.MouseEvent 是 react 提供鼠标事件类型。

是因为 react 里的事件是被 react 处理过的,和原生事件不一样。

直接给 document 绑定事件,这时候 event 是 MouseEvent 类型:

而在 jsx 里绑定事件,这时候 event 是 React.MouseEvent 类型:

我们都要支持:

这两个 state 一个是保存 offset 的,一个是保存是否在拖动中的标记的:

然后先把之前的事件监听器去掉:

在 mousedown 的时候绑定 mousemove 和 mouseup 事件:

mousemove 的时候根据 event 修改 offset。

mouseup 的时候去掉事件监听器。

这个过程中还要修改记录拖动状态的 flag 的值。

然后实现拖动过程中的 offset 的计算:

 

javascript

复制代码

const updateOffset: EventHandle = e => { const scrollXOffset = document.documentElement.scrollLeft || document.body.scrollLeft; const scrollYOffset = document.documentElement.scrollTop || document.body.scrollTop; const pageX = e.pageX - scrollXOffset; const pageY = e.pageY - scrollYOffset; const { x: rectX, y: rectY, width, height } = containerRef.current!.getBoundingClientRect(); const { width: targetWidth, height: targetHeight } = targetRef.current!.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.y : offsetY, }; setOffsetValue(calcOffset); onDragChange?.(calcOffset); };

首先 e.pageX 和 e.pageY 是距离页面顶部和左边的距离。

减去 scrollLeft 和 scrollTop 之后就是离可视区域顶部和左边的距离了。

然后减去 handler 圆点点的半径。

这样算出来的就是按住 handler 圆点的中心拖动的效果。

但是拖动不能超出 container 的区域,所以用 Math.max 来限制在 0 到 width、height 之间拖动。

这里如果传入的 direction 参数是 x,那么就只能横向拖动,是为了下面的 Slider 准备的:

我们来试下效果:

 

javascript

复制代码

import { useRef, type FC } from 'react'; import { Color } from './color'; import Handler from './Handler'; import Transform from './Transform'; import useColorDrag from './useColorDrag'; const Palette: FC<{ color: Color }> = ({ color }) => { const transformRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null); const [offset, dragStartHandle] = useColorDrag({ containerRef, targetRef: transformRef, onDragChange: offsetValue => { console.log(offsetValue); } }); return ( <div ref={containerRef} className="color-picker-panel-palette" onMouseDown={dragStartHandle} > <Transform ref={transformRef} offset={{x: offset.x, y: offset.y}}> <Handler color={color.toRgbString()}/> </Transform> <div className={`color-picker-panel-palette-main`} style={{ backgroundColor: `hsl(${color.toHsl().h},100%, 50%)`, backgroundImage: 'linear-gradient(0deg, #000, transparent),linear-gradient(90deg, #fff, hsla(0, 0%, 100%, 0))', }} /> </div> ); }; export default Palette;

可以看到,滑块可以拖动了,并且只能在容器范围内拖动。

只是颜色没有变化,这个需要根据 x、y 的值来算出当前的颜色。

我们封装个工具方法:

新建 ColorPicker/utils.ts

 

javascript

复制代码

import { TransformOffset } from "./Transform"; import { Color } from "./color"; export const calculateColor = (props: { offset: TransformOffset; containerRef: React.RefObject<HTMLDivElement>; targetRef: React.RefObject<HTMLDivElement>; color: Color; }): Color => { const { offset, targetRef, containerRef, color } = props; const { width, height } = containerRef.current!.getBoundingClientRect(); const { width: targetWidth, height: targetHeight } = targetRef.current!.getBoundingClientRect(); const centerOffsetX = targetWidth / 2; const centerOffsetY = targetHeight / 2; const saturation = (offset.x + centerOffsetX) / width; const lightness = 1 - (offset.y + centerOffsetY) / height; const hsv = color.toHsv(); return new Color({ h: hsv.h, s: saturation <= 0 ? 0 : saturation, v: lightness >= 1 ? 1 : lightness, a: hsv.a, }); }

这块逻辑就是用 x/width 用 y/height 求出一个比例来。

当然,x、y 还要加上圆点的半径,这样才是中心点位置。

然后在 onDragChange 里根据 offset 计算当前的颜色,并且通过 onChange 回调返回新颜色。

在 ColorPickerPanel 组件里处理下 onChange:

 

javascript

复制代码

function onPaletteColorChange(color: Color) { setColorValue(color); onChange?.(color); }

修改当前颜色,并且调用它的 onChange 回调函数。

测试下:

没啥问题。

只是现在初始的颜色不对:

最开始也要计算一次滑块位置:

我们给 useColorDrag 添加 color 和 calculate 两个参数。

最开始和 color 改变的时候,调用 calculate 计算位置,重新设置 offsetValue。

 

javascript

复制代码

import { useEffect, useRef, useState } from 'react'; import { TransformOffset } from './Transform'; import { Color } from './color'; type EventType = | MouseEvent | React.MouseEvent<Element, MouseEvent> type EventHandle = (e: EventType) => void; interface useColorDragProps { offset?: TransformOffset; color: Color; containerRef: React.RefObject<HTMLDivElement>; targetRef: React.RefObject<HTMLDivElement>; direction?: 'x' | 'y'; onDragChange?: (offset: TransformOffset) => void; calculate?: () => TransformOffset; } function useColorDrag( props: useColorDragProps, ): [TransformOffset, EventHandle] { const { offset, color, targetRef, containerRef, direction, onDragChange, calculate, } = props; const [offsetValue, setOffsetValue] = useState(offset || { x: 0, y: 0 }); const dragRef = useRef({ flag: false }); useEffect(() => { if (dragRef.current.flag === false) { const calcOffset = calculate?.(); if (calcOffset) { setOffsetValue(calcOffset); } } }, [color]); useEffect(() => { document.removeEventListener('mousemove', onDragMove); document.removeEventListener('mouseup', onDragStop); }, []); const updateOffset: EventHandle = e => { const scrollXOffset = document.documentElement.scrollLeft || document.body.scrollLeft; const scrollYOffset = document.documentElement.scrollTop || document.body.scrollTop; const pageX = e.pageX - scrollXOffset; const pageY = e.pageY - scrollYOffset; const { x: rectX, y: rectY, width, height } = containerRef.current!.getBoundingClientRect(); const { width: targetWidth, height: targetHeight } = targetRef.current!.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.y : offsetY, }; setOffsetValue(calcOffset); onDragChange?.(calcOffset); }; const onDragStop: EventHandle = e => { document.removeEventListener('mousemove', onDragMove); document.removeEventListener('mouseup', onDragStop); dragRef.current.flag = false; }; const onDragMove: EventHandle = e => { e.preventDefault(); updateOffset(e); }; const onDragStart: EventHandle = e => { document.addEventListener('mousemove', onDragMove); document.addEventListener('mouseup', onDragStop); dragRef.current.flag = true; }; return [offsetValue, onDragStart]; } export default useColorDrag;

然后在调用的时候传入这两个参数:

 

javascript

复制代码

const [offset, dragStartHandle] = useColorDrag({ containerRef, targetRef: transformRef, color, onDragChange: offsetValue => { const newColor = calculateColor({ offset: offsetValue, containerRef, targetRef: transformRef, color }); onChange?.(newColor); }, calculate: () => { return calculateOffset(containerRef, transformRef, color) } });

这里的 calculateOffset 在 utils.ts 里定义:

 

javascript

复制代码

export const calculateOffset = ( containerRef: React.RefObject<HTMLDivElement>, targetRef: React.RefObject<HTMLDivElement>, color: Color ): TransformOffset => { const { width, height } = containerRef.current!.getBoundingClientRect(); const { width: targetWidth, height: targetHeight } = targetRef.current!.getBoundingClientRect(); const centerOffsetX = targetWidth / 2; const centerOffsetY = targetHeight / 2; const hsv = color.toHsv(); return { x: hsv.s * width - centerOffsetX, y: (1 - hsv.v) * height - centerOffsetY, }; };

就是根据 hsv 里的 s 和 v 的百分比乘以 width、height,计算出 x、y,然后减去滑块的宽高的一半。

可以看到,现在初始位置就对了:

我在 App.tsx 里设置个不同的颜色:

 

html

复制代码

<ColorPickerPanel value="rgb(166 57 57)"></ColorPickerPanel>

初始位置也是对的:

我们在下面加一个颜色块:

 

html

复制代码

<div style={{width: 20, height: 20, background: colorValue.toRgbString()}}></div>

可以看到,随着滑块的移动,返回的颜色是对的。

案例代码上传了小册仓库

总结

这节我们实现了 ColorPicker 的调色板。

它的布局不复杂,就是一个渐变的背景,加上一个绝对定位的滑块。

就是根据位置计算颜色、根据颜色计算位置,这两个方向的计算比较复杂。

根据位置计算颜色,以 x 方向为例:

需要用 mousemove 时的 e.pageX(距离文档左边的距离) 减去 scrollLeft 计算出滑块距离视口的距离,然后减去容器距离视口的距离,再减去滑块半径就是滑块距离容器的距离 x。

然后用这个 x 除以 width 计算出 hsv 中的 s 的值。

这样就根据拖拽位置计算出了颜色。

根据颜色计算位置比较简单,直接拿到 hsv 的 s 和 v 的值,根据百分比乘以 width、height 就行。

此外,颜色我们用的 @ctrl/tinycolor 这个包的颜色类,antd 也是用的这个。但是参数不用直接传 Color 类的实例,可以传 rgb、string 等我们内部转成 Color 类。

这样,调色板部分就完成了。

  • 8
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: 您可以使用value属性来为antd rangepicker赋值。例如: ```jsx import React, { useState } from 'react'; import { DatePicker } from 'antd'; const { RangePicker } = DatePicker; function App() { const [dateRange, setDateRange] = useState(['2022-01-01', '2022-01-31']); const handleDateChange = (dates) => { setDateRange(dates); }; return ( <div> <RangePicker value={dateRange} onChange={handleDateChange} /> </div> ); } export default App; ``` 在上面的例子中,我们使用useState hook来定义一个名为dateRange的状态变量,并将其初始值设置为['2022-01-01', '2022-01-31']。然后,我们将这个变量作为value属性传递给RangePicker组件,这样就可以为antd rangepicker赋值了。当用户选择日期范围时,我们可以通过onChange事件处理程序来更新dateRange状态变量。 ### 回答2: antd RangePicker 是一个日期区间选择器,常用于筛选一个时间段的数据。要对 RangePicker 赋值,需要使用 value 属性。 value 属性接受一个数组,数组的第一个元素是开始日期,第个元素是结束日期。例如: ``` javascript import React, { useState } from 'react'; import { RangePicker } from 'antd'; import moment from 'moment'; function App() { const [dateRange, setDateRange] = useState([moment('2021-01-01'), moment('2021-01-31')]); function handleDateChange(date) { setDateRange(date); } return ( <div> <RangePicker value={dateRange} onChange={handleDateChange} /> </div> ); } export default App; ``` 在这个例子中,我们先导入了 RangePicker 和 moment 库。通过 useState 定义了一个 dateRange 状态变量,初始值为 2021 年 1 月 1 日到 2021 年 1 月 31 日的时间段。在 RangePicker 组件中,我们将 value 属性设置为 dateRange,这样就会在组件渲染时显示这个时间段。当用户修改时间段时,会触发 onChange 事件,调用 handleDateChange 函数更新 dateRange 状态变量的值,从而实现了 RangePicker 的赋值功能。 需要注意的是,value 属性只用于初始化时设置默认值,之后的修改都应该通过 onChange 事件来实现。如果想要清空 RangePicker 的值,只需要将 value 属性设置为 null 或 undefined 即可。 ### 回答3: AntD是一款基于React的UI组件库,其中RangePicker组件可以让用户在一个时间范围内进行选择。在使用RangePicker时,我们可以通过设置value属性来给这个组件赋值。 下面我们来详细介绍AntD RangePicker的赋值方法: 1.使用moment对象 我们可以使用moment对象来设置RangePicker的value属性,如下所示: ```jsx import React from 'react'; import { RangePicker } from 'antd'; import moment from 'moment'; // 引入moment对象 function App() { const value = [moment('2022-01-01', 'YYYY-MM-DD'), moment('2022-01-08', 'YYYY-MM-DD')]; // moment对象数组 return ( <RangePicker value={value} /> ); } export default App; ``` 上述代码中,我们定义了一个moment对象数组value,包含了RangePicker组件需要展示的开始时间和结束时间,最后将这个数组赋值给value属性即可。 2.使用字符串数组 除了moment对象,我们也可以直接使用字符串数组来设置RangePicker的value属性,如下所示: ```jsx import React from 'react'; import { RangePicker } from 'antd'; function App() { const value = ['2022-01-01', '2022-01-08']; // 字符串数组 return ( <RangePicker value={value} /> ); } export default App; ``` 上述代码中,我们定义了一个字符串数组value,包含了RangePicker组件需要展示的开始时间和结束时间,最后将这个数组赋值给value属性即可。 需要注意的是,当使用字符串数组时,需要保证数组中的字符串格式与我们所设定的格式一致(如'YYYY-MM-DD'),否则会造成赋值失败。 综上所述,我们可以使用moment对象或者字符串数组来对AntD RangePicker进行赋值。在实际开发中,我们可以根据具体业务需求来选择合适的赋值方式。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

人工智能_SYBH

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值