[address] (www.yuque.com/docs/share/… React-quill Rich Text Editor & Mining

Due to the service function, it is necessary to implement when users can format and paste content in rich text. If users copy pictures, they need to upload the pictures to the server and insert them into the text. Seemingly reasonable request, but there are many holes.

Begin to use

Install the react – quill

npm install react-quill @ beta


Making the address

Quill Chinese document address

Quill document address

Get hands-on with Codeopen

1, custom rich text format brush function

Quill does not have the function of format brush, so there is no corresponding icon. In this case, you need to customize the toolbar. The Quill toolbar control can be defined with an array of format names, or can be defined with a normal HTML container. The HTML container definition is used here.

First define a format brush icon. The icon here uses the Unicode reference of Iconfont, which supports dynamic adjustment of icon size, color and so on according to font. You can highlight it and keep it consistent with the original toolbar interaction.

const CustomButton = () => <i className="iconfont">&#xe608; </i>;Copy the code

Customize the HTML container CustomToolbar

const CustomToolbar = () => (
  <div id="toolbar">
    <select className="ql-header" defaultValue={''} onChange={(e) => e.persist()}>
      <option value="1" />
      <option value="2" />
      <option value="3" />
      <option value="4" />
      <option value="5" />
      <option selected />
    <select className="ql-size" defaultValue={''} onChange={(e) => e.persist()}>
      <option value="small" />
      <option selected />
      <option value="large" />
      <option value="huge" />
    <button className="ql-formatBrush">
      <CustomButton />
    <button className="ql-bold" />
    <button className="ql-italic" />
    <button className="ql-underline" />
    <button className="ql-strike" />
    <select className="ql-color" />
    <select className="ql-background" />
    <select className="ql-align" />
    <button className="ql-list" value="ordered" />
    <button className="ql-list" value="bullet" />
    <button className="ql-indent" value="+1" />
    <button className="ql-indent" value="-1" />
    <button className="ql-image" />
    <button className="ql-clean" />
Copy the code

CustomToolbar is to add the original toolbar with a format brush toolbar. Put custom toolbars in return.

return ( <div className={styles['rich-text']}> <CustomToolbar /> <ReactQuill ref={editorRef} placeholder={placeholder || 'Please input content '} modules={modules} theme="snow" {... props} value={value} /> </div> );Copy the code


Format brush function

Next comes formatting, which is done in Modules. FormatBrush is consistent with the toolbar className=” qL-FormatBrush “.

// remember to declare the variable let quillEditor = null; const copyFormatting = { value: 'un-active', format: {}, }; //modules method const modules = useMemo(() => {return {toolbar: {// container: toolbarContainer, container: '#toolbar', handlers: { image: imageHandler, formatBrush: () => { copyFormatBrush(quillEditor, copyFormatting); }},}}; } []);Copy the code

Key format setting style method, click Format brush, if the selected area has a style, then save the style, the format brush function is selected, click Delete style again, deselect; Set the format style. These three methods can be kept in a separate file export management.

/* eslint-disable @typescript-eslint/no-use-before-define */ export const copyFormatBrush = (quillEditor, // Click format brush, if there is a selected area and style, then save the style, key state changed to Selected. // Click again, delete style, key to deselect. if (copyFormatting.value === 'un-active') { const range = quillEditor.getSelection(true); if (range == null || range.length === 0) return; const format = quillEditor.getFormat(range); if (Object.keys(format).length === 0) return; setCopyFormatting(quillEditor, copyFormatting, 'active', format); } else { setCopyFormatting(quillEditor, copyFormatting, 'un-active', null); }}; / / set copyFormatting: Export const setCopyFormatting = (quill, copyFormatting, value, format) => { copyFormatting.value = value; copyFormatting.format = format; const toolbar = quill.getModule('toolbar').container; const brushBtn = toolbar.querySelector('.ql-formatBrush'); if (value === 'active') { brushBtn.classList.add('ql-formatBrushactive'); quill.on('selection-change', pasteFormatHandler); } else { brushBtn.classList.remove('ql-formatBrushactive'); quill.off('selection-change', pasteFormatHandler); } function pasteFormatHandler(range, oldRange, source) { return pasteFormat(range, oldRange, source, quill, copyFormatting); }}; // Paste style handler: Paste the style if the range is selected and there is a save style, Initialize copyFormatting export const pasteFormat = (range, oldRange, source, quill, copyFormatting) => { if (range && copyFormatting.format) { if (range.length === 0) { } else { quill.formatText(range.index, range.length + 1, copyFormatting.format); setCopyFormatting(quill, copyFormatting, 'un-active', null); } } else { // console.log('Cursor not in the editor') } };Copy the code

I tested using the default QL-active highlighting style, it doesn’t work, only custom format brush highlighting style:

  .ql-toolbar.ql-snow .ql-formatBrushactive {
      color: #06c;
Copy the code


The above format brush function has been implemented.

2, content paste operation – picture preview directly

The content copied by users contains cross-domain images, which need to be uploaded to the server and inserted into the text for display.

At present, the image function is customized encapsulation. When users insert images, they Upload them through ANTD Upload, and then insert the URL images of our server after uploading image resources to the server.

Customize Quill’s behavior and functionality

The main method is imageHandler, which uses the Quill. Register module to customize the behavior and functionality of Quill.

const modules = useMemo(() => { return { toolbar: { // container: toolbarContainer, container: '#toolbar', handlers: { image: imageHandler, }, }, }; } []);Copy the code

The logic of the popover is not much to show you. After you have uploaded the fileList data, insert the image in the rich text.

JSX import './ImageBlot'; Const insertImages = (fileList = []) => {if (quillEditor && typeof quillEditor. InsertEmbed === 'function') { quillEditor.focus(); const unprivilegedEditor = editorRef.current.makeUnprivilegedEditor(quillEditor); fileList.forEach((file) => { setTimeout(() => { const range = unprivilegedEditor.getSelection(); const position = range ? range.index : 0; quillEditor.insertEmbed(position, 'image', { ossid: file.ossId, url: file.url, }); quillEditor.setSelection(position + 1, 0); }, 0); }); }}; // return <ImageUploadModal onCancel={() => { setShowImageUpload(false); }} onCreate={(fileList) => { setShowImageUpload(false); insertImages(fileList); }} / >Copy the code

The upload of copy content and pictures to get the URL data of the new server is also realized in the custom image module.

import { Quill } from 'react-quill'; import { getTmpUrl } from '@/components/AliyunOSSUpload/service'; import { isExpired } from '@/components/AliyunOSSUpload/utils'; import { saveAliOssImg } from '@/services/global'; import errorImg from './error-img.png'; const BlockEmbed = Quill.import('blots/block/embed'); const getExpireFromUrl = (url) => { const index = url.indexOf('? '); if (index ! == -1) { const reg = new RegExp('(^|&)Expires=([^&]*)(&|$)', 'i'); const str = url.substring(index + 1, url.length); const expire = str.match(reg)[2]; return expire || 0; } return 0; }; const replacedImage = (ossId, node) => { getTmpUrl({ ossIds: [ossId] }).then((data) => { if (data && data.length > 0 && data[0].tmpUrl) { const url = data[0].tmpUrl; node.setAttribute('src', url); }}); }; /** * custom image tags, Add ossId */ class ImageBlot extends BlockEmbed {static create(value) {console.log('ImageBlot :>> ', value); const node = super.create(); if (value.ossid && value.url) { node.setAttribute('ossid', value.ossid); node.setAttribute('src', value.url); const expire = getExpireFromUrl(value.url); if (expire && isExpired(parseInt(expire, 10))) { replacedImage(value.ossid, node); If (value.url) {let sourceType = 'URL'; let content = value.url; if (value.url.indexOf('base64') > -1) { sourceType = 'BASE64'; content = value.url.split('base64,')[1]; } saveAliOssImg({sourceType, content }) .then((data) => { if (data && data.url) { node.setAttribute('src', data.url); } }) .catch(() => { node.setAttribute('src', errorImg); }); } console.log('ImageBlot --node-:>> ', node); return node; } static value(node) { const ossId = node.getAttribute('ossid'); const url = node.getAttribute('src'); return { ossid: ossId, url, }; } } ImageBlot.blotName = 'image'; ImageBlot.tagName = 'img'; Quill.register(ImageBlot);Copy the code

The actual operation

1. Copy resources outside the domain:

2, paste rich text effect:

From the output, the final result of the substitution is no problem

Add image loading failure, local path image resource loading failure processing, using image placeholder.

Known issues

1. “Directly copy the pictures in the local folder, ok under MAC, invalid under Windows”, check the original Copy under Windows is BOLB stream (back-end server failed to accept), but under MAC is File stream. Further exploration found that in the Windows folder system, copy text class things, is in the clipboard, can be obtained; However, the copied image file, no matter right click copy, or Ctrl + C copy will not work. At present, Windows copy pictures can choose rich text insert picture function implementation. (Please leave comments and discuss)

2. Roll the page to the bottom after pasting. The solution is as follows:

Add the following styles to the less file.

.ql-clipboard {
      position: fixed;
      // display: none;
      left: 50%;
      top: 50%;
Copy the code

Welcome a lot of message discussion, a good plan can leave valuable comments.