React项目添加react-quill富文本编辑器,遇到的问题,比如hr标签丢失
1.引入相关插件
yarn add react-quill
yarn add quill-image-resize-module
我下载的版本是
"quill-image-resize-module": "^3.0.0","react-quill": "^2.0.0",
2项目引入
话不多说直接上效果,上代码【自行查找自己查找的代码吧】
import React, { PureComponent, Component } from 'react';
import ReactQuill, { Quill } from 'react-quill';
import { each, isArray, isEqual, isObject, map } from 'lodash';
import 'react-quill/dist/quill.snow.css';
import 'react-quill/dist/quill.bubble.css';
import { services } from '@comall-backend-builder/core';
// @ts-ignore
import loadingUrl from './loading.gif';
import { message, Modal, Tooltip as AntTooltip } from 'antd';
import { ApiRequestConfig, ApiUploadConfig } from '@comall-backend-builder/core/lib/services/api';
import { VideoModalContent } from '../../../../components/rich-text-plus/video-modal-content';
import { language } from '@comall-backend-builder/core/lib/services';export interface EditorModulesProps {/*** 工具栏id*/toolbarId: string;
}export interface EditorProps {/*** 图片提示*/imagePlaceholder: string;/*** 图片上传配置*/uploadConfig: ApiRequestConfig & ApiUploadConfig;/*** 当前值*/value: string;className?: string;style?: React.CSSProperties;/*** 占位提示*/placeholder?: string;/*** 是否禁用*/disabled?: boolean;/*** 输入组件的 name,作为该输入组件在其所属表单内的唯一识别符*/name: string;/*** 内容改变回调* @param value 新值* @param name 输入组件的 name,作为该输入组件在其所属表单内的唯一识别符*/onChange: (value: string, name: string) => void;
}declare var window: Window & { Quill: any };const SIZE_LIST = ['10px','12px','14px','16px','18px','20px','22px','24px','26px','28px','30px','32px','34px','36px',
];const richTextFormats = ['header','bold','italic','underline','strike','blockquote','code-block','list','bullet','script','indent','direction','size','color','background','font','align','link','image','video','hr',
];
const modules = [[{ header: [1, 2, 3, 4, 5, 6, 0] }],['bold', 'italic', 'underline', 'strike'],['blockquote', 'code-block'],[{ header: '1' }, { header: '2' }],[{ list: 'ordered' }, { list: 'bullet' }],[{ script: 'sub' }, { script: 'super' }],[{ indent: '-1' }, { indent: '+1' }],[{ direction: 'rtl' }],[{ size: SIZE_LIST }],[{ color: [] }, { background: [] }],[{ font: [] }],[{ align: [] }],['link', 'image'],['video'],['clean'],
];//quill的调整图片大小模块需要全局的quill变量
window.Quill = Quill;const ImageResize = require('quill-image-resize-module');
Quill.register('modules/ImageResize', ImageResize);
const Size = Quill.import('attributors/style/size');
Size.whitelist = SIZE_LIST;
Quill.register(Size, true);const BlockEmbed = Quill.import('blots/block/embed');
class DividerBlot extends BlockEmbed {static blotName = 'hr';static tagName = 'hr';
}
Quill.register(DividerBlot);function getObjectFirstKey(object: any) {return Reflect.ownKeys(object)[0];
}let id = 0;
function generateToolbarId() {const toolbarId = 'toolbar-' + id;id++;return toolbarId;
}class EditorModules extends PureComponent<EditorModulesProps> {/*** 渲染具有下拉选项的toolbar* @param {String} keyName quill中的toolbar名称,需要渲染成ql-{keyName}的格式* @param {Array} value* @return {ReactElement}*/static renderSelectableBar(keyName: string, value: any) {return (<span><select className={`ql-${keyName}`}>{map(value, (option, optionIndex) => (<option key={optionIndex} value={option} />))}</select></span>);}/*** 渲染format中的每一个toolbar以及其对应的tooltip* @param {Object|String} format* @param {Number} toolIndex* @return {ReactElement}*/static renderToolBarWithTip(format: any, toolIndex: string) {let renderedComponent;let keyName = '';let tipKey = '';if (!isObject(format)) {keyName = format;tipKey = format;//最简单的字符串,渲染成指定class的buttonrenderedComponent = <button className={`ql-${keyName}`} />;} else {keyName = getObjectFirstKey(format) as string;// @ts-ignoreconst value = format[keyName];if (isArray(value)) {tipKey = keyName;//对象值为数组时,渲染成指定class的下拉barrenderedComponent = EditorModules.renderSelectableBar(keyName, value);} else {tipKey = `${keyName}${value}`;//对象值为非数组时,渲染成指定class并带有特定值的buttonrenderedComponent = <button className={`ql-${keyName}`} value={value} />;}}const TOOLTIPS = services.language.getText('components.RichText.tooltips');// @ts-ignoreconst title = TOOLTIPS[tipKey];return (<AntTooltip key={toolIndex} title={title}>{renderedComponent}</AntTooltip>);}/*** 根据modules中的每一项渲染一组format,* @return {Array<ReactElement>}* @param {Array} formats*/static renderFormats(formats: any) {return map(formats, (format, toolIndex) =>EditorModules.renderToolBarWithTip(format, toolIndex));}render() {const { toolbarId } = this.props;return (<div id={toolbarId}>{map(modules, (formats, formatIndex) => (<span key={formatIndex} className="ql-formats">{EditorModules.renderFormats(formats)}</span>))}</div>);}
}export class RichTextNew extends Component<EditorProps> {quillRef: any;formats: string[];toolbarId: string;richTextModules: {[key: string]: any;};static defaultProps = {theme: 'snow',disabled: false,imagePlaceholder: loadingUrl,};createQuillRef = (e: any) => (this.quillRef = e);/*** 富文本上传图片时默认是base64编码,需要将其进行文件上传,并根据上传返回的图片地址显示在富文本中*/imageHandler = () => {let { imagePlaceholder, uploadConfig } = this.props;const input = document.createElement('input');input.setAttribute('type', 'file');input.setAttribute('accept', 'image/*');input.setAttribute('class', 'hide');input.setAttribute('multiple', 'multiple');document.body.appendChild(input);input.click();const editor = this.quillRef.getEditor();input.onchange = (event) => {const target = event.target as HTMLInputElement;const files = target.files;const range = editor.getSelection(true);let success = 0;let finish = 0;//防止清空loading图后索引变化导致删除的位置错误,此处记录并在请求结束后统一进行删除let errorIndexs: any = [];each(files, (_file, index) => {const cursor = index + range.index;//在此放置一个过渡用内容,暂时只支持图片形式的过渡editor.insertEmbed(cursor, 'image', imagePlaceholder);editor.setSelection(cursor + 1);services.api.upload({ files: files![index] }, uploadConfig).then((response) => {editor.deleteText(cursor, 1);editor.insertEmbed(cursor, 'image', response.path);success++;}).catch(() => {//请求失败后也要清空loading图,防止误解仍在上传中。errorIndexs.push(cursor);}).finally(() => {finish++;if (finish >= files!.length) {//删除时需要从大到小的进行删除,所以先排序each(errorIndexs.sort((before: any, after: any) => after - before),(errorIndex) => {editor.deleteText(errorIndex, 1);});//为了确保所有请求完毕后,光标处于正确的位置editor.setSelection(range.index + success);}});});};document.body.removeChild(input);};videoUrl: string | undefined;onVideoUrlChange = (url?: string) => {this.videoUrl = url;};clearVideoUrl = () => {this.videoUrl = undefined;};handleVideo = () => {return new Promise((resolve, reject) => {if (!this.videoUrl) {message.warning(language.getText('videoAddressOrUploadVideo'));reject();return;}const editor = this.quillRef.getEditor();const range = editor.getSelection(true);const cursor = range.index;editor.insertEmbed(cursor, 'video', this.videoUrl);editor.setSelection(cursor + 2);resolve(null);}).finally(() => {this.clearVideoUrl();});};videoHandler = () => {Modal.confirm({content: <VideoModalContent onChange={this.onVideoUrlChange} />,icon: null,okText: language.getText('common.ok'),cancelText: language.getText('common.cancel'),title: language.getText('uploadVideo'),onCancel: this.clearVideoUrl,onOk: this.handleVideo,});};constructor(props: any) {super(props);this.formats = richTextFormats;this.toolbarId = generateToolbarId();this.handleChange = this.handleChange.bind(this);this.richTextModules = {imageResize: {},toolbar: {container: `#${this.toolbarId}`,handlers: {image: this.imageHandler,video: this.videoHandler,},},};}handleChange(html: any) {const { onChange, name } = this.props;if (onChange) {onChange(html, name);}}shouldComponentUpdate(nextProps: any, _nextState: any) {return !isEqual(this.props, nextProps);}render() {const {value,placeholder = services.language.getText('common.pleaseInput'),disabled,className,style,} = this.props;let props = {theme: disabled ? 'bubble' : 'snow',onChange: this.handleChange,modules: this.richTextModules,formats: this.formats,placeholder: placeholder,readOnly: disabled,className: className,style: style,value: value || '',ref: this.createQuillRef,};return (<div className="quill-editor"><EditorModules toolbarId={this.toolbarId} /><ReactQuill {...props} /></div>);}
}
在开发中,遇到的问题有:
1.问题:历史数据中有hr标签,但是react-quill给自动过滤了,导致hr水平线丢失
解决方式:formats添加了hr,但并不能解决问题,还缺失Quill.register(DividerBlot);,添加上即可完美解决。(具体代码看上方)