基于 quill 实现的富文本编辑器

# 图例

An image

# props

  • value
    • 富文本编辑器的内容,这个内容是经过 quill 转换过的,并不是直接可读的内容
  • onChange
  • theme
    • 主题色,默认 'snow', 代表白色
  • placeholder
  • readOnlyreadOnly
    • readOnly 只读
  • bounds
    • 挂载的 dom 元素
  • modules
    • quill 富文本编辑器的配置
  • className

# 实现代码

点击 展开/收起 jsx 代码
/**
 * @description 自定义 QuillTextEditor 组件
 */

import React, { useEffect, useRef } from 'react';
import classnames from 'classnames';
import Quill, { RangeStatic } from 'quill';
import { isDelta, isValuesEqual } from './utils';
import 'quill/dist/quill.snow.css';
import 'quill/dist/quill.bubble.css';

const QuillTextEditor = (props) => {
  const {
    theme = 'snow',
    className,
    value,
    onChange,
    bounds,
    modules,
    placeholder = '请输入',
    readOnly,
  } = props;

  // 创建一个ref来存储editor的DOM元素
  const contanierRef = useRef(null);
  // 存储Quill实例的ref
  const quillRef = useRef(null);

  // 设置编辑器的内容,但不触发onChange
  const silentUpdate = (content) => {
    const editor = quillRef.current;
    if (isDelta(value)) {
      editor.setContents(value);
    } else if (typeof value === 'string') {
      editor.setContents(editor.clipboard.convert(content));
    }
  };

  useEffect(() => {
    if (contanierRef.current) {
      const editor = new Quill(contanierRef.current, {
        theme,
        modules,
        placeholder,
        readOnly: false,
        bounds,
      });
      quillRef.current = editor;

      // 初始加载时设置内容
      silentUpdate(value);

      // 注册编辑器的变更回调函数
      const handleChange = () => {
        const currentContent = editor.root.innerHTML;
        onChange?.(currentContent);
      };

      // 注册 编辑器的改变回调函数   调用传入的onChange回调函数,传递当前内容
      editor.on('text-change', handleChange);
      return () => {
        editor.off('text-change', handleChange);
      };
    }
  }, []);

  // 当外部控制的value改变时,更新编辑器内容
  useEffect(() => {
    if (quillRef.current && !isValuesEqual(value, getEditorInnerHtml())) {
      const selection = quillRef.current.getSelection(); // 保存当前的选择区域
      quillRef.current.setContents(
        quillRef.current.clipboard.convert(value),
        'silent',
      );
      if (selection) {
        // 如果有选区,恢复选区
        quillRef.current.setSelection(selection);
      }
    }
  }, [value]);

  useEffect(() => {
    if (quillRef.current) {
      if (readOnly) {
        quillRef.current?.disable();
      } else {
        quillRef.current?.enable();
      }
    }
  }, [readOnly]);

  // 获取编辑器内部 html 元素内容
  const getEditorInnerHtml = () => {
    return quillRef.current?.root.innerHTML;
  };

  return (
    <div
      className={classnames('quill-text-editor ql-editor-container', className)}
    >
      <div ref={contanierRef} />
    </div>
  );
};

export default QuillTextEditor;

/**
 * 使用示例
 */

const test = () => {
  return (
    <QuillTextEditor
      className="day-book-quill"
      value={text}
      onChange={setText}
      readOnly={readOnly}
      bounds={`#id .ql-editor-container`}
      modules={{
        // syntax: true,
        toolbar: {
          container: [
            // [{ 'header': 1 }, { 'header': 2 }], // 标题 —— 独立平铺
            // [{header: [1, 2, 3, 4, 5, 6, false]}], // 标题 —— 下拉选择
            // [{size: ["small", false, "large", "huge"]}], // 字体大小
            [{ list: 'ordered' }, { list: 'bullet' }], // 有序、无序列表
            // ["blockquote", "code-block"], // 引用  代码块
            ['blockquote'], // 引用  代码块
            // 链接按钮需选中文字后点击
            // ["link", "image", "video"], // 链接、图片、视频
            ['link', 'image'], // 链接、图片、视频
            [{ align: [] }], // 对齐方式// text direction
            // [{indent: "-1"}, {indent: "+1"}], // 缩进
            // ["bold", "italic", "underline", "strike"], // 加粗 斜体 下划线 删除线
            ['bold', 'underline', 'strike'], // 加粗 斜体 下划线 删除线
            [{ color: [] }, { background: [] }], // 字体颜色、字体背景颜色
            // [{'script': 'sub'}, {'script': 'super'}],      // 下标/上标
            // [{'font': []}],//字体
            ['clean'], // 清除文本格式
          ],
        },
      }}
    />
  );
};


点击 展开/收起 css 代码
.quill-text-editor.ql-editor-container {
  .ql-toolbar {
    padding: 6px 8px;

    .ql-formats {
      margin-right: 36px;

      button {
        .ql-stroke {
          stroke: var(--black45-opcity1);
        }

        &:hover {
          color: var(--brand);

          .ql-stroke {
            stroke: var(--brand);
          }
        }

        &.ql-link::after {
          border-right: 0;
          padding-left: 6px;
          content: '链接';
        }

        &.ql-clean::after {
          border-right: 0;
          padding-left: 6px;
          content: '清空样式';
        }
      }
    }
  }

  .ql-container {
    .ql-editor {
      font-size: 12px;

      a {
        color: var(--brand);
      }

      &::before {
        font-style: normal;
        color: var(--black45);
        font-size: 12px;
      }
    }

    .ql-tooltip input[data-link]::placeholder {
      opacity: 0;
      font-style: normal;
    }

    .ql-tooltip[data-mode='link']::before {
      content: '网址:';
      color: var(--black45);
    }

    .ql-preview {
      color: var(--brand);
    }

    .ql-tooltip.ql-editing a.ql-action::after {
      border-right: 0;
      content: '保存';
      padding-right: 0;
      color: var(--brand);
    }

    .ql-tooltip::before {
      content: '网址:';
      line-height: 26px;
      margin-right: 8px;
      color: var(--black45);
    }

    .ql-tooltip a.ql-action::after {
      border-right: 1px solid var(--grey);
      content: '编辑';
      margin-left: 16px;
      padding-right: 8px;
      color: var(--brand);
    }

    .ql-tooltip a.ql-remove::before {
      content: '移除';
      margin-left: 8px;
      color: var(--brand);
    }
  }
}


点击 展开/收起 配置变量 代码
/**
 * @des 一些变量
 */

const titleConfig = [
  { Choice: '.ql-bold', title: '加粗' },
  { Choice: '.ql-italic', title: '斜体' },
  { Choice: '.ql-underline', title: '下划线' },
  { Choice: '.ql-header', title: '段落格式' },
  { Choice: '.ql-strike', title: '删除线' },
  { Choice: '.ql-blockquote', title: '块引用' },
  { Choice: '.ql-code', title: '插入代码' },
  { Choice: '.ql-code-block', title: '插入代码段' },
  { Choice: '.ql-font', title: '字体' },
  { Choice: '.ql-size', title: '字体大小' },
  { Choice: '.ql-list[value="ordered"]', title: '编号列表' },
  { Choice: '.ql-list[value="bullet"]', title: '项目列表' },
  { Choice: '.ql-direction', title: '文本方向' },
  { Choice: '.ql-header[value="1"]', title: 'h1' },
  { Choice: '.ql-header[value="2"]', title: 'h2' },
  { Choice: '.ql-align', title: '对齐方式' },
  { Choice: '.ql-color', title: '字体颜色' },
  { Choice: '.ql-background', title: '背景颜色' },
  { Choice: '.ql-image', title: '图像' },
  { Choice: '.ql-video', title: '视频' },
  { Choice: '.ql-link', title: '添加链接' },
  { Choice: '.ql-formula', title: '插入公式' },
  { Choice: '.ql-clean', title: '清除字体格式' },
  { Choice: '.ql-script[value="sub"]', title: '下标' },
  { Choice: '.ql-script[value="super"]', title: '上标' },
  { Choice: '.ql-indent[value="-1"]', title: '向左缩进' },
  { Choice: '.ql-indent[value="+1"]', title: '向右缩进' },
  { Choice: '.ql-header .ql-picker-label', title: '标题大小' },
  { Choice: '.ql-header .ql-picker-item[data-value="1"]', title: '标题一' },
  { Choice: '.ql-header .ql-picker-item[data-value="2"]', title: '标题二' },
  { Choice: '.ql-header .ql-picker-item[data-value="3"]', title: '标题三' },
  { Choice: '.ql-header .ql-picker-item[data-value="4"]', title: '标题四' },
  { Choice: '.ql-header .ql-picker-item[data-value="5"]', title: '标题五' },
  { Choice: '.ql-header .ql-picker-item[data-value="6"]', title: '标题六' },
  { Choice: '.ql-header .ql-picker-item:last-child', title: '标准' },
  { Choice: '.ql-size .ql-picker-item[data-value="small"]', title: '小号' },
  { Choice: '.ql-size .ql-picker-item[data-value="large"]', title: '大号' },
  { Choice: '.ql-size .ql-picker-item[data-value="huge"]', title: '超大号' },
  { Choice: '.ql-size .ql-picker-item:nth-child(2)', title: '标准' },
  { Choice: '.ql-align .ql-picker-item:first-child', title: '居左对齐' },
  {
    Choice: '.ql-align .ql-picker-item[data-value="center"]',
    title: '居中对齐',
  },
  {
    Choice: '.ql-align .ql-picker-item[data-value="right"]',
    title: '居右对齐',
  },
  {
    Choice: '.ql-align .ql-picker-item[data-value="justify"]',
    title: '两端对齐',
  },
];