富文本定制扩展玩不明白?用Quill2实现react富文本定制扩展!

1,022 阅读3分钟

Quill2官网 quilljs.com/docs/quicks…

官网上有用react的例子

image.png

基于例子进行代码改造

先写一个Quill组件

import Quill from 'quill';
import { v4 as uuidV4 } from 'uuid';
import ossService from '@/services/oss';
import { message } from 'antd';
import { useEffect } from 'react';
import styles from './index.less';

type QuillComponentProps = {
  defaultValue: string;
  onChange: (value: string) => void;
};

let quill: any = null;


const QuillComponent = (props: QuillComponentProps) => {
  const { defaultValue, onChange } = props;

  const toolbarOptions = {
    container: [
      ['bold', 'italic', 'underline', 'strike'], // 加粗,斜体,下划线,删除线
      ['blockquote', 'code-block'], // 引用,代码块
      [{ header: 1 }, { header: 2 }], // 标题,键值对的形式;1、2表示字体大小
      [{ list: 'ordered' }, { list: 'bullet' }], // 列表
      [{ direction: 'rtl' }], // 文本方向
      [{ header: [1, 2, 3, 4, 5, 6, false] }], // 几级标题
      [{ color: [] }, { background: [] }], // 字体颜色,字体背景颜色
      [{ font: [] }], // 字体
      [{ align: [] }], // 对齐方式
      ['clean'], // 清除字体样式
      ['image', 'video', 'link'],
    ], // 上传图片、上传视频               // remove formatting button
  };

  //富文本配置
  const options = {
    modules: {
      toolbar: toolbarOptions,
    },
    placeholder: '请输入...',
    theme: 'snow',
  };

  useEffect(() => {
    quill = new Quill('#editor', options);
  }, []);

  useEffect(() => {
    if (quill) {
      if (defaultValue === '<p><br></p>') {
        quill.setContents([{ insert: '' }]);
        return;
      }
      quill.setContents(JSON.parse(defaultValue));
      quill.on('text-change', (delta, oldDelta, source) => {
        // 在这里处理富文本内容的变化
        const content = quill.getContents();
        onChange(JSON.stringify(content));
 
      });
    }
  }, [quill, defaultValue]);

  return (
    <div className={styles.content}>
      <div id="editor" />
    </div>
  );
};
export default QuillComponent;

自定义配置

自定义配置上传图片,视频,链接等,Quill也有详细说明 quilljs.com/docs/guides…

image.png

简单改造一下,改成ts文件

import Quill from 'quill';

const BlockEmbed = Quill.import('blots/block/embed');

class VideoBlot extends BlockEmbed {
  static blotName = 'video';
  static tagName = 'video';

  static create(url) {
    let node = super.create();
    node.setAttribute('src', url);
    node.setAttribute('controls', 'true');
    return node;
  }

  static value(node: HTMLElement) {
    return node.getAttribute('src');
  }
}

export default VideoBlot;

上面这个是自定义视频上传,写成VideoBlot之后,需要在Quill里注册一下

Quill.register(VideoBlot);

然后在toolbarOptions里配置下handlers

handlers: {
  video: () => {
  //你想实现的自定义上传视频
  }
  }

实现文档

上传视频之后要插入文档,就要用到

image.png 注意!!

我查看Quill2官网没找到有html字符串可以变成Delta格式的方法(如果有,并且我这段话有误请指出) 只有Delta格式变成html字符串的方法 quill.root.innerHTML 然而在Quill内部运行的只支持Deltas

image.png

所以我存数据会直接存Delta

// 在这里处理富文本内容的变化
const content = quill.getContents();
onChange(JSON.stringify(content));

赋值的时候

quill.setContents(JSON.parse(defaultValue));

最后

完整代码

QuillComponent.tsx

import Quill from 'quill';
import { v4 as uuidV4 } from 'uuid';
import ossService from '@/services/oss';
import { message } from 'antd';
import { useEffect } from 'react';
import VideoBlot from '@/pages/system/guideQuill/VideoBlot';
import styles from './index.less';

type QuillComponentProps = {
  defaultValue: string;
  onChange: (value: string) => void;
};

let quill: any = null;

Quill.register(VideoBlot);
const QuillComponent = (props: QuillComponentProps) => {
  const { defaultValue, onChange } = props;

  const toolbarOptions = {
    container: [
      ['bold', 'italic', 'underline', 'strike'], // 加粗,斜体,下划线,删除线
      ['blockquote', 'code-block'], // 引用,代码块
      [{ header: 1 }, { header: 2 }], // 标题,键值对的形式;1、2表示字体大小
      [{ list: 'ordered' }, { list: 'bullet' }], // 列表
      [{ direction: 'rtl' }], // 文本方向
      [{ header: [1, 2, 3, 4, 5, 6, false] }], // 几级标题
      [{ color: [] }, { background: [] }], // 字体颜色,字体背景颜色
      [{ font: [] }], // 字体
      [{ align: [] }], // 对齐方式
      ['clean'], // 清除字体样式
      ['image', 'video', 'link'],
    ], // 上传图片、上传视频               // remove formatting button

    handlers: {
      video: () => {
        let range = quill.getSelection(true);
        quill.insertText(range.index, '\n', Quill.sources.USER);
        //上传视频

        const fileInput = document.createElement('input');
        fileInput.setAttribute('type', 'file');
        fileInput.setAttribute('accept', 'video/*'); // 只接受视频文件
        fileInput.style.display = 'none'; // 隐藏文件输入

        // 将<input>添加到文档中
        document.body.appendChild(fileInput);

        // 触发<input>的click事件,这会打开文件选择对话框
        fileInput.click();

        // 当用户选择文件后,你可以监听<input>的change事件来处理文件
        fileInput.addEventListener('change', (event) => {
          const file = event.target?.files[0];
          if (file) {
            // 这里处理选择的文件
            const filePath =
              'documentAttachment/wms/' + uuidV4() + file?.name.slice(file.name.indexOf('.'));
            ossService.upload({ bucketACL: 'PUBLIC_READ', key: filePath }).then((uploadRes) => {
              if (uploadRes?.status?.success && uploadRes?.body?.preSignedUrl) {
                try {
                  fetch(uploadRes.body.preSignedUrl, {
                    method: 'PUT',
                    headers: {
                      'Content-Type': 'application/octet-stream',
                    },
                    body: file,
                  }).then((res) => {
                    if (res.status === 200) {
                      quill.insertEmbed(
                        range.index + 1,
                        'video',
                        uploadRes?.body?.preSignedUrl.split('?')[0],
                        Quill.sources.USER,
                      );
                      quill.formatText(range.index + 1, 1);
                      quill.setSelection(range.index + 2, Quill.sources.SILENT);
                      fileInput.remove();
                    }
                  });
                } catch (e) {
                  message.warn('上传失败');
                  fileInput.remove();
                }
              }
            });
          } else {
            console.log('No file selected.');
          }
        });

        console.log('视频');
      },
    },
  };

  //富文本配置
  const options = {
    modules: {
      toolbar: toolbarOptions,
    },
    placeholder: '请输入...',
    theme: 'snow',
  };

  useEffect(() => {
    quill = new Quill('#editor', options);
  }, []);

  useEffect(() => {
    if (quill) {
      if (defaultValue === '<p><br></p>') {
        quill.setContents([{ insert: '' }]);
        return;
      }
      quill.setContents(JSON.parse(defaultValue));
      quill.on('text-change', (delta, oldDelta, source) => {
        // 在这里处理富文本内容的变化
        const content = quill.getContents();
        onChange(JSON.stringify(content))
        //直接得到html字符串
        console.log('Content changed:', quill.root.innerHTML);
      });
    }
  }, [quill, defaultValue]);

  return (
    <div className={styles.content}>
      <div id="editor" />
    </div>
  );
};
export default QuillComponent;

VideoBlot.ts

import Quill from 'quill';

const BlockEmbed = Quill.import('blots/block/embed');

class VideoBlot extends BlockEmbed {
  static blotName = 'video';
  static tagName = 'video';

  static create(url) {
    let node = super.create();
    node.setAttribute('src', url);
    node.setAttribute('controls', 'true');
    return node;
  }

  static value(node: HTMLElement) {
    return node.getAttribute('src');
  }
}

export default VideoBlot;

index.tsx

import 'quill/dist/quill.snow.css';
import QuillComponent from './QuillComponent';
import { useEffect, useState } from 'react';

const RichText = () => {
  const [detailInfo, setDetailInfo] = useState<any>();
  const [currentHtml, setCurrentHtml] = useState('');

  useEffect(() => {
    //通过接口拿到数据
    setDetailInfo({ content: '' });
  }, []);

  return (
    <div>
      <QuillComponent
        defaultValue={detailInfo?.content || '123'}
        onChange={(val) => {
          setCurrentHtml(val);
        }}
      />
    </div>
  );
};

export default RichText;

index.less 做了个操作栏的固定

.content {
  :global {
    .ql-toolbar .ql-snow {
      position: fixed !important;
    }
  }
}

本文皆原创,如需转发请告知一声喔! over...