quill编辑器使用

3,475 阅读3分钟

编辑器是个正经编辑器,就是文档太不正经了

基本的一些使用文档还是可以的,基础用例代码如下

import React, { useEffect } from 'react';
import Quill, { QuillOptionsStatic } from 'quill';
import 'quill/dist/quill.snow.css';
import { TranslatorWrapper } from './translator';

const QuillEditor = () => {
  useEffect(() => {
    const toolbarOptions = [
      [{ header: [false, 1, 2, 3, 4, 5, 6] }],
      ['bold', 'italic', 'underline', 'strike'],
      [{ indent: '-1' }, { indent: '+1' }],
      [{ align: '' }, { align: 'center' }, { align: 'right' }],
      ['link'],
      ['image'],
    ];
    const options: QuillOptionsStatic = {
      // boundsDOM元素或者一个DOM元素的css选择器,其中编辑器的UI元素(例如:tooltips)应该被包含其中。目前,只考虑左右边界 
      // 这个属性很管用,特别是在一些布局中,不设置边界的话,ql-tooltip会溢出编辑器,导致被遮挡,并且有些时候,布局已经确定,z-index都不能这解决遮挡问题
      bounds: document.getElementById('quill_editor') as HTMLElement,
      debug: 'false',
      modules: {
        imageUpload: true,
        toolbar: toolbarOptions,
      },
      placeholder: '',
      theme: 'snow',
    };
    const editor = new Quill('#quill_editor', options);
    setQuillEditor(editor);
  }, []);

  return (
    <TranslatorWrapper>
      <div id="quill_editor"></div>
    </TranslatorWrapper>
  );
};

export default QuillEditor;

基本样例展示:

关于quill Toolbar中header中文展示处理

基于styled-components给quill编辑器的主容器增加一个包裹 TranslatorWrapper,通过修改::before属性,达到修改中文的目的; 那如果需要国际化,就可以通过参数处理来完成了

<TranslatorWrapper>
  <div id="quill_editor"></div>
</TranslatorWrapper>

样式代码:translator.js

export const TranslatorWrapper = styled.div`
  .ql-snow .ql-picker.ql-header .ql-picker-label::before,
  .ql-snow .ql-picker.ql-header .ql-picker-item::before {
    content: '正文';
    font-size: 14px;
  }
  .ql-snow .ql-picker.ql-header .ql-picker-label[data-value='1']::before,
  .ql-snow .ql-picker.ql-header .ql-picker-item[data-value='1']::before {
    content: '标题 1';
    font-size: 14px;
  }
  .ql-snow .ql-picker.ql-header .ql-picker-label[data-value='2']::before,
  .ql-snow .ql-picker.ql-header .ql-picker-item[data-value='2']::before {
    content: '标题 2';
    font-size: 14px;
  }
  .ql-snow .ql-picker.ql-header .ql-picker-label[data-value='3']::before,
  .ql-snow .ql-picker.ql-header .ql-picker-item[data-value='3']::before {
    content: '标题 3';
    font-size: 14px;
  }
  .ql-snow .ql-picker.ql-header .ql-picker-label[data-value='4']::before,
  .ql-snow .ql-picker.ql-header .ql-picker-item[data-value='4']::before {
    content: '标题 4';
    font-size: 14px;
  }
  .ql-snow .ql-picker.ql-header .ql-picker-label[data-value='5']::before,
  .ql-snow .ql-picker.ql-header .ql-picker-item[data-value='5']::before {
    content: '标题 5';
    font-size: 14px;
  }
  .ql-snow .ql-picker.ql-header .ql-picker-label[data-value='6']::before,
  .ql-snow .ql-picker.ql-header .ql-picker-item[data-value='6']::before {
    content: '标题 6';
    font-size: 14px;
  }
`;

关于React自定义组件的插入使用

注:这里的场景只是上传图片,所以对原来的上传图片按钮进行了替换,直接替换成一个React组件,其他新增一个按钮的自定义场景这次没有涉及

在前面基础应用代码中新增

import React, { useEffect } from 'react';
...
import ImageUploader from './ImageUploader.tsx';

const Image = Quill.import('formats/image');
Image.className = 'img-fluid';
Quill.register(Image, true);
Quill.register('modules/imageUpload', ImageUploader);

const QuillEditor = () => {
...

图片上传模块 ImageUploader.tsx

import React from 'react';
import ReactDOM from 'react-dom';
import Quill, { QuillOptionsStatic } from 'quill';
import _ from 'lodash';
// 需要注入的React组件
import ImgButton from './ImgButton';

interface ImageUploaderInterface {
  quill: Quill;
  options: QuillOptionsStatic;
}
class ImageUploader implements ImageUploaderInterface {
  quill: Quill;
  options: QuillOptionsStatic;
  constructor(quill: Quill, options: QuillOptionsStatic) {
    this.quill = quill;
    this.options = options;

    // 获取editor的toolbar
    const toolbar = this.quill.getModule('toolbar');

    // 获取toolbar中原来 上传图片按钮
    const imageParent = _.toArray(toolbar.container?.children).find(
      (child) => child?.children[0]?.className === 'ql-image'
    );
    // 覆盖原有按钮 DOM结构
    ReactDOM.render(<ImgButton editor={this.quill} onChange={this.onImageChange} />, imageParent);
  }

  // 传入图片上传ImgButton组件的回调方法,与editor通信
  onImageChange = (src: string) => {
    this.quill.getSelection(true);
    const index = (this.quill.getSelection() || {}).index || this.quill.getLength();
    this.quill.insertEmbed(index, 'image', src);
  };
}

export default ImageUploader;

ql-tooltip被遮挡 z-index设置也无效时

注意检查是否设置了bounds属性

 bounds: document.getElementById('quill_editor') as HTMLElement,

hanlder添加

文档提供方式:

var quill = new Quill('#editor', {
  modules: {
    toolbar: {
      container: '#toolbar',  // Selector for toolbar container
      handlers: {
        'bold': customBoldHandler
      }
    }
  }
});

toolbar使用数组方式设置的话,增加handler方式如下

var toolbar = quill.getModule('toolbar');
toolbar.addHandler('bold', customBoldHandler);

修改添加链接的placeholder

/**
 * 修改添加链接的placeholder
 */
useEffect(() => {
  document
    .querySelector('input[data-link]')
    ?.setAttribute('data-link', '请输入链接 http(s)://...');
}, [quillEditor]);

quill snow link tooltip 中输入框对输入的链接进行校验(validation)

utils.js

import Emitter from 'quill/core/emitter';
import { message } from 'antd';

/**
 * 覆写 Snow 的tooltip的save
 */
export function SnowTooltipSave() {
  const { value } = this.textbox;
  const linkValidityRegex = /^(http|https)/;
  switch (this.root.getAttribute('data-mode')) {
    case 'link': {
      if (!linkValidityRegex.test(value)) {
        message.error('链接格式错误,请输入链接 http(s)://...');
        return;
      }
      const { scrollTop } = this.quill.root;
      if (this.linkRange) {
        this.quill.formatText(this.linkRange, 'link', value, Emitter.sources.USER);
        delete this.linkRange;
      } else {
        this.restoreFocus();
        this.quill.format('link', value, Emitter.sources.USER);
      }
      this.quill.root.scrollTop = scrollTop;
      break;
    }
    default:
  }
  this.textbox.value = '';
  this.hide();
}

export function SnowThemeLinkHandler(value) {
  if (value) {
    const range = this.quill.getSelection();
    if (range == null || range.length === 0) return;
    let preview = this.quill.getText(range);
    if (/^\S+@\S+\.\S+$/.test(preview) && preview.indexOf('mailto:') !== 0) {
      preview = `mailto:${preview}`;
    }
    const { tooltip } = this.quill.theme;
    tooltip.save = SnowTooltipSave;
    tooltip.edit('link', preview);
  } else {
    this.quill.format('link', false);
  }
}

用例,基于 基础用例 代码

import React, { useEffect } from 'react';
import Quill, { QuillOptionsStatic } from 'quill';
...
const SnowTheme = Quill.import('themes/snow');
SnowTheme.DEFAULTS.modules.toolbar.handlers.link = SnowThemeLinkHandler;
...
const QuillEditor = () => {
...

欢迎扫码关注作者公众号