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的button
            renderedComponent = <button className={`ql-${keyName}`} />;
        } else {
            keyName = getObjectFirstKey(format) as string;
            // @ts-ignore
            const value = format[keyName];
            if (isArray(value)) {
                tipKey = keyName;
                //对象值为数组时,渲染成指定class的下拉bar
                renderedComponent = EditorModules.renderSelectableBar(keyName, value);
            } else {
                tipKey = `${keyName}${value}`;
                //对象值为非数组时,渲染成指定class并带有特定值的button
                renderedComponent = <button className={`ql-${keyName}`} value={value} />;
            }
        }
        const TOOLTIPS = services.language.getText('components.RichText.tooltips');
        // @ts-ignore
        const 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);,添加上即可完美解决。(具体代码看上方)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值