函数式组件 -- 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;
相关推荐
第七玩家6 小时前
React-异步队列执行方法useSyncQueue
前端·javascript·react.js
IT、木易14 小时前
大白话react第十六章React 与 WebGL 结合的实战项目
前端·react.js·webgl
市民中心的蟋蟀16 小时前
第十六章 React中常用的的错误处理方法 【下】
前端·javascript·react.js
ffiyu17 小时前
【React进阶系列第三课】组件函数什么时候被执行
前端·react.js
ffiyu17 小时前
【React进阶系列第二课】JSX 与 React Element
前端·react.js
ffiyu17 小时前
【React 进阶系列第一课】组件就是函数
前端·react.js
市民中心的蟋蟀19 小时前
第十六章 React中常用的的错误处理方法 【上】
前端·javascript·react.js
Bigger19 小时前
useEffect 的底层是如何实现的?(美团面试原题)
前端·react.js·面试
winyh520 小时前
从零开始封装React UI 组件库并发布到NPM
前端·react.js·前端框架
蠟筆小新工程師1 天前
React Native 建構apps的好處在哪裡
javascript·react native·react.js