函数式组件 -- ColorPicker (但具有历史记录功能)

函数式组件 -- ColorPicker (但具有历史记录功能)

封装了一个用来选择颜色的react函数式组件ColorPicker,使用自定义钩子函数,利用useState和localStorage实现记录选中颜色历史功能。

1. ColorPicker的外部依赖

ColorPicker组件使用antd的Popover组件,react-color的SketchPicker组件,以及styled-components的styled实现的.

typescript 复制代码
import React from 'react';
import styled from 'styled-components';
import { SketchPicker, ColorChangeHandler, ColorResult } from 'react-color';
import { Popover } from 'antd';

2. 自定义钩子函数实现颜色历史记录功能

自定义钩子函数useLocalStorageColor的本质是对useState的加强;在记录颜色的同时将最新选中的颜色值更新到持久化存储器中去。难点在于将数据处理成第三方组件能够方便使用的格式。

typescript 复制代码
function useLocalStorageColor(token: string, init: Array<string>, split: string) {
  // 初始化颜色序列
  const originPreset: any = init;
  // 从localStorage尝试获取颜色值
  const fromStorage = (localStorage.getItem(token) || '').split(split);
  // 容器
  const emptyBlock: any[] = [];
  // 暂存storage中的值
  const [colorArr, setColorArr] = React.useState(fromStorage);
  // 长度
  const len = colorArr.length;
  // 剩余位置
  const blank = originPreset.length - len;

  // 如果还有剩余的位置
  if (blank) {
    // 将剩余的位置都使用空白色填充
    for (let i = 0; i < blank; i++) {
      emptyBlock.push({ color: `#CCCCC${i}`, title: 'sketch-picker-preset' });
    }
  }

  // 更新拼接颜色数组
  const setColor = React.useCallback((newData: string) => {
    // token是话题名称
    const oldColor = (localStorage.getItem(token) || '').split(split);
    // 查询当前选中颜色是否已经记录在历史中了
    const index = oldColor.findIndex((v) => v === newData);
    // 如果已经存在了则需要从历史中清除
    if (index !== -1) oldColor.splice(index, 1);
    // 更新之后的颜色序列
    const updateData = [newData, ...oldColor.slice(0, originPreset.length - 1)];
    // 将新的颜色历史存入localStorage中
    localStorage.setItem(token, updateData.join(split));
    // 更新颜色值
    setColorArr(updateData);
  }, []);

  // 自定义钩子函数会将预置颜色,本地颜色和空白格拼接起来返回
  const color = originPreset.concat(colorArr).concat(emptyBlock);
  // 返回加强之后的useState
  return [color, setColor];
}

3. ColorPicker组件的外部接口

typescript 复制代码
interface IProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'> {
  id?: string;
  color: string;
  disabled?: boolean;
  onChange?: ColorChangeHandler;
}

4. 内部包装组件C

使用styled-components的styled函数构造一个自定义样式的包裹div,这个过程可以使用less+className代替; 使用css-in-js的方案原因在于:传参方便!

typescript 复制代码
const C = styled.div<{ disabled: boolean }>`
  line-height: 1;
  position: relative;
  height: 20px;
  cursor: ${(props) => (props.disabled ? 'no-drop' : 'cursor')};
  .disable-pointer-events {
    pointer-events: ${(props) => (props.disabled ? 'none' : 'auto')};
  }
  .color {
    display: inline-block;
    width: 32px;
    height: 20px;
    border: ${(props) => (props.disabled ? '1px dashed #c4c4c4' : '1px solid #333')};
    border-radius: 2px;
    padding: 2px;
    .color-show {
      height: 100%;
    }
  }
`;

5. SketchPicker的change回调函数

SketchPicker组件上提供了两种回调onChangeonChangeComplete。前者是选择的颜色变化之后的回调,而后者是做了节流的颜色变化之后的回调。之所以这样做是为了将耗时操作和频繁操作分开以提高性能。

typescript 复制代码
  const handleChange = (data: ColorResult, e: React.ChangeEvent<HTMLInputElement>) => {
    if (onChange) {
      // 执行传入的回调函数
      onChange(data, e);
    }
  };

  // 确认选中颜色发生变化之后的回调函数(在此处的功能是将当前选中的颜色值写入历史中去)
  const handleChangeComplete = (data: ColorResult) => {
    // 将选中的颜色写入localStorage中去
    localStorage.setItem('currentColor', data.hex);
    // 尝试性获取选中颜色对应的div
    const target = document.querySelector(`div[title="${data.hex.toUpperCase()}"]`) as HTMLElement;
    if (target) {
      // 如果获取到对应的dom,那就给这个dom加上边框
      target.style.boxShadow = '0px 0px 0px 2px #2A54D1';
    }
  };

6. SketchPicker的预置颜色

SketchPicker提供了名为presetColors的接口,接受一个string[]类型的变量,显示数组中的颜色;这个是颜色历史记录功能的支柱!

typescript 复制代码
    <SketchPicker
        presetColors={presetColors} // 预置颜色值
        color={color} // 受控组件
        onChange={handleChange} // 回调1
        onChangeComplete={handleChangeComplete} // 回调2
    />

7. Popover设置

自定义antd组件Popover的content trigger destroyTooltipOnHide onOpenChange属性,使之符合期望的效果

typescript 复制代码
    <Popover
        content={
        // 弹出的组件为react-color中的SketchPicker
        <SketchPicker
            presetColors={presetColors} // 预置颜色值
            color={color} // 受控组件
            onChange={handleChange} // 回调1
            onChangeComplete={handleChangeComplete} // 回调2
        />
        }
        trigger="click" // Popover组件渲染content的时机
        destroyTooltipOnHide // 隐藏时是否销毁
        onOpenChange={() => {
        // 打开状态发生变化的时候的回调函数
        const currentColor = localStorage.getItem('currentColor');
        if (currentColor) setPresetColors(currentColor); // 向内存中存储当前颜色值
        }}
    >
        {/* 背景或者外框 */}
        <div className="color">
        <div className="color-show" style={{ backgroundColor: disabled ? 'transparent' : color }} />
        </div>
    </Popover>

8. 完整的组件 ColorPicker.tsx

typescript 复制代码
import React from 'react';
import styled from 'styled-components';
import { SketchPicker, ColorChangeHandler, ColorResult } from 'react-color';
import { Popover } from 'antd';

// 自定义的钩子函数
function useLocalStorageColor(token: string, init: Array<string>, split: string) {
  // 初始化颜色序列
  const originPreset: any = init;
  // 从localStorage尝试获取颜色值
  const fromStorage = (localStorage.getItem(token) || '').split(split);
  // 容器
  const emptyBlock: any[] = [];
  // 暂存storage中的值
  const [colorArr, setColorArr] = React.useState(fromStorage);
  // 长度
  const len = colorArr.length;
  // 剩余位置
  const blank = originPreset.length - len;

  // 如果还有剩余的位置
  if (blank) {
    // 将剩余的位置都使用空白色填充
    for (let i = 0; i < blank; i++) {
      emptyBlock.push({ color: `#CCCCC${i}`, title: 'sketch-picker-preset' });
    }
  }

  // 更新拼接颜色数组
  const setColor = React.useCallback((newData: string) => {
    // token是话题名称
    const oldColor = (localStorage.getItem(token) || '').split(split);
    // 查询当前选中颜色是否已经记录在历史中了
    const index = oldColor.findIndex((v) => v === newData);
    // 如果已经存在了则需要从历史中清除
    if (index !== -1) oldColor.splice(index, 1);
    // 更新之后的颜色序列
    const updateData = [newData, ...oldColor.slice(0, originPreset.length - 1)];
    // 将新的颜色历史存入localStorage中
    localStorage.setItem(token, updateData.join(split));
    // 更新颜色值
    setColorArr(updateData);
  }, []);

  // 自定义钩子函数会将预置颜色,本地颜色和空白格拼接起来返回
  const color = originPreset.concat(colorArr).concat(emptyBlock);
  // 返回加强之后的useState
  return [color, setColor];
}

interface IProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'> {
  id?: string;
  color: string;
  disabled?: boolean;
  onChange?: ColorChangeHandler;
}

const C = styled.div<{ disabled: boolean }>`
  line-height: 1;
  position: relative;
  height: 20px;
  cursor: ${(props) => (props.disabled ? 'no-drop' : 'cursor')};
  .disable-pointer-events {
    pointer-events: ${(props) => (props.disabled ? 'none' : 'auto')};
  }
  .color {
    display: inline-block;
    width: 32px;
    height: 20px;
    border: ${(props) => (props.disabled ? '1px dashed #c4c4c4' : '1px solid #333')};
    border-radius: 2px;
    padding: 2px;
    .color-show {
      height: 100%;
    }
  }
`;

const XColorPicker = ({ id, color, onChange, disabled, ...props }: IProps): JSX.Element => {
  // 选中颜色发生变化之后的回调函数(一般由调用者传入)
  const handleChange = (data: ColorResult, e: React.ChangeEvent<HTMLInputElement>) => {
    if (onChange) {
      // 执行传入的回调函数
      onChange(data, e);
    }
  };

  // 确认选中颜色发生变化之后的回调函数(在此处的功能是将当前选中的颜色值写入历史中去)
  const handleChangeComplete = (data: ColorResult) => {
    // 将选中的颜色写入localStorage中去
    localStorage.setItem('currentColor', data.hex);
    // 尝试性获取选中颜色对应的div
    const target = document.querySelector(`div[title="${data.hex.toUpperCase()}"]`) as HTMLElement;
    if (target) {
      // 如果获取到对应的dom,那就给这个dom加上边框
      target.style.boxShadow = '0px 0px 0px 2px #2A54D1';
    }
  };

  // 使用自定义钩子函数初始化颜色历史值
  const [presetColors, setPresetColors] = useLocalStorageColor('colorHistory', Array(8).fill(''), '&');

  // 返回由div包裹的Popover组件
  return (
    <C
      id={id} // 使用document查找此dom的依据
      disabled={disabled || false} // 状态,和样式相关
      {...props} // 其它属性
    >
      {/* 总体的结构是div>span>Popover */}
      <span className="disable-pointer-events">
        <Popover
          content={
            // 弹出的组件为react-color中的SketchPicker
            <SketchPicker
              presetColors={presetColors} // 预置颜色值
              color={color} // 受控组件
              onChange={handleChange} // 回调1
              onChangeComplete={handleChangeComplete} // 回调2
            />
          }
          trigger="click" // Popover组件渲染content的时机
          destroyTooltipOnHide // 隐藏时是否销毁
          onOpenChange={() => {
            // 打开状态发生变化的时候的回调函数
            const currentColor = localStorage.getItem('currentColor');
            if (currentColor) setPresetColors(currentColor); // 向内存中存储当前颜色值
          }}
        >
          {/* 背景或者外框 */}
          <div className="color">
            <div className="color-show" style={{ backgroundColor: disabled ? 'transparent' : color }} />
          </div>
        </Popover>
      </span>
    </C>
  );
};

export default XColorPicker;
相关推荐
PleaSure乐事4 分钟前
【React.js】AntDesignPro左侧菜单栏栏目名称不显示的解决方案
前端·javascript·react.js·前端框架·webstorm·antdesignpro
getaxiosluo5 分钟前
react jsx基本语法,脚手架,父子传参,refs等详解
前端·vue.js·react.js·前端框架·hook·jsx
阿伟来咯~2 小时前
记录学习react的一些内容
javascript·学习·react.js
吕彬-前端3 小时前
使用vite+react+ts+Ant Design开发后台管理项目(五)
前端·javascript·react.js
学前端的小朱3 小时前
Redux的简介及其在React中的应用
前端·javascript·react.js·redux·store
bysking4 小时前
【前端-组件】定义行分组的表格表单实现-bysking
前端·react.js
September_ning8 小时前
React.lazy() 懒加载
前端·react.js·前端框架
web行路人8 小时前
React中类组件和函数组件的理解和区别
前端·javascript·react.js·前端框架
番茄小酱0018 小时前
Expo|ReactNative 中实现扫描二维码功能
javascript·react native·react.js
Rattenking10 小时前
React 源码学习01 ---- React.Children.map 的实现与应用
javascript·学习·react.js