wangeditor-team / wangEditor

wangEditor, open-source Web rich text editor 开源 Web 富文本编辑器
http://www.wangeditor.com/
MIT License
17.58k stars 3.32k forks source link

wangEditor5.1.23版本,使用自定义上传多次上传报错 #5959

Open longsiping opened 1 month ago

longsiping commented 1 month ago

bug 描述

使用wangEditor5.1.23版本,customUpload自定义上传功能,调用insertFn方法插入富文本内时,只上传一张图片没啥问题,上传完成后,再继续上传一张,就提示子节点无法找到;[Unhandled Rejection (Error): Cannot find a descendant at path [0,4] in node: {"children"](global.js:1 Uncaught (in promise) Error: Cannot find a descendant at path [1] in node: {"children":[{"type":"paragraph","children":[{"text":"1231"}]}],"operations":[],"selection":{"anchor":{"path":[1,0],"offset":0},"focus":{"path":[1,0],"offset":0}},"marks":null,"id":"wangEditor-10","isDestroyed":false,"isFullScreen":false,"history":{"undos":[[{"type":"split_node","path":[0,0],"position":4,"properties":{}},{"type":"split_node","path":[0],"position":1,"properties":{"type":"paragraph"}}],[{"type":"remove_text","path":[0,0],"offset":0,"text":"1231"},{"type":"remove_node","path":[0],"node":{"type":"paragraph","children":[{"text":""}]}},{"type":"remove_node","path":[0],"node":{"type":"paragraph","children":[{"text":""}]}},{"type":"insert_node","path":[0],"node":{"type":"paragraph","children":[{"text":"1231"}]}},{"type":"set_selection","properties":{"anchor":{"path":[0,0],"offset":4},"focus":{"path":[0,0],"offset":4}},"newProperties":{"anchor":{"path":[1,0],"offset":0},"focus":{"path":[1,0],"offset":0}}}]],"redos":[]}})

代码



## 你预期的样子是?

*请输入内容……*

## 系统和浏览器及版本号

- 操作系统
- 浏览器和版本

## wangEditor 版本
5.1.23
*请输入内容……*

## demo 能否复现该 bug ?
能
能/不能

- 中文 demo https://www.wangeditor.com/demo/
- English demo https://www.wangeditor.com/demo/?lang=en

## 在线 demo

*请尽量提供在线 demo (推荐以下网站),帮助我们最低成本复现 bug*

- https://codesandbox.io/
- https://codepen.io/
- https://stackblitz.com/

## 最小成本的复现步骤

(请告诉我们,如何最快的复现该 bug)

- 步骤一
- 步骤二
- 步骤三
longsiping commented 1 month ago

import React, { memo, useState, useEffect, useCallback } from "react"; import { Editor, Toolbar } from '@wangeditor/editor-for-react'; import { IDomEditor, IEditorConfig, IToolbarConfig, DomEditor } from '@wangeditor/editor'; import '@wangeditor/editor/dist/css/style.css'; import { message, Spin } from '@alipay/bigfish/antd'; import { uploadFile } from "@/services/upload_file";

type InsertFnType = (url: string, alt: string, href: string) => void

interface getFileUrlType { id: number; orderId: number; filePosition: string; moduleType: string; fileType: string; fileName: string; fileSize: string; gmtCreate: string; gmtModified: string; url: string; };

interface MonoContext { html: string; setHtml: (html: string) => void; flag?: boolean; width?: number; height?: number; onChange?: (value: string) => void; maxLength: number; isUploadImage?: boolean; orderId?: string; filePosition?: string; moduleType?: string; getWork?: () => void; setCurrentImage?: (value: any) => void; };

export default memo((props: MonoContext) => {

const { html, setHtml, flag, width = 480, height = 400, onChange, isUploadImage = false, orderId, filePosition, moduleType, getWork, setCurrentImage } = props;

const [loading, setLoading] = useState<boolean>(false);
const [editor, setEditor] = useState<IDomEditor | null>(null);

//工具栏配置
const toolbarConfig: Partial<IToolbarConfig> = {
    //排除掉哪些功能
    // excludeKeys: [
    //     'group-video',
    //     'group-image',
    //     "sup",
    //     "sub",
    //     "fullScreen",
    //     "insertTable",
    //     "fontFamily",
    //     'undo',
    //     'redo'

    // ],
    //重新配置某些功能
    toolbarKeys: [
        "headerSelect",
        "fontSize",
        "bold",
        "italic",
        "underline",
        "through",
        "color",
        "bgColor",
        // "lineHeight",
        {
            key: "group-more-style",
            title: '更多',
            menuKeys: [
                "divider",
                "codeBlock",
                "bulletedList",
                "numberedList",
                // "todo",
                // "code",
                "insertLink",
                'blockquote',
                isUploadImage ? "uploadImage" : '',
                "insertTable",
                "fullScreen"
            ],
            iconSvg: `<svg viewBox="0 0 1024 1024"><path d="M204.8 505.6m-76.8 0a76.8 76.8 0 1 0 153.6 0 76.8 76.8 0 1 0-153.6 0Z"></path><path d="M505.6 505.6m-76.8 0a76.8 76.8 0 1 0 153.6 0 76.8 76.8 0 1 0-153.6 0Z"></path><path d="M806.4 505.6m-76.8 0a76.8 76.8 0 1 0 153.6 0 76.8 76.8 0 1 0-153.6 0Z"></path></svg>`
        }

    ]
    // modalAppendToBody:true

};

//编辑器配置
const editorConfig: Partial<IEditorConfig> = {
    placeholder: '请输入内容...',
    readOnly: flag, //是否只读
    MENU_CONF: {
        'fontSize': {
            fontSizeList: [
                { name: '初号', value: '56px' },
                { name: '小初', value: '48px' },
                { name: '一号', value: '34px' },
                { name: '小一', value: '32px' },
                { name: '二号', value: '29px' },
                { name: '小二', value: '24px' },
                { name: '三号', value: '21px' },
                { name: '小三', value: '20px' },
                { name: '四号', value: '18px' },
                { name: '小四', value: '16px' },
                { name: '五号', value: '14px' },
                { name: '小五', value: '12px' },
            ]
        },
        // 'fontFamily': {
        //     fontFamilyList: [
        //         {
        //             name: '黑体', value: '黑体'
        //         },
        //         {
        //             name: '楷体', value: '楷体'
        //         },
        //         {
        //             name: '仿宋', value: '仿宋'
        //         },
        //         {
        //             name: '宋体', value: '宋体'
        //         }
        //     ]
        // },
        // 'lineHeight': {
        //     lineHeightList: ['1', '1.5', '2', '2.5']
        // },
        //上传图片的拦截
        'uploadImage': isUploadImage ? {
            fieldName: 'file',
            allowedFileTypes: ['image/*'],
            //server: '/api/v1/napa/web/transformWorkOrder/uploadFile',
            maxNumberOfFiles: 2,
            // meta: {
            //     orderId,
            //     filePosition,
            //     moduleType
            // },
            // headers: {
            //     accept: 'application/json'
            // },
            //自定义上传逻辑,
            customUpload: async (file: File, insertFn: InsertFnType) => {
                const formData = new FormData();
                formData.append('file', file);
                formData.append('orderId', orderId);
                formData.append('filePosition', filePosition);
                formData.append('moduleType', moduleType);
                const res: any = await uploadFile(formData, {}).catch((err) => {
                    message.error('上传失败');
                }).finally(() => setLoading(false));
                if (res.success) {
                    message.success('上传成功');
                    if (typeof getWork === 'function') {
                        const data: any = await getWork();
                        const filerFile = data?.bkBankFileUploadInfoModels?.filter((item: getFileUrlType) => item.filePosition == filePosition) ?? [];
                        if (filerFile?.length > 0) {
                            const url = filerFile[filerFile.length - 1].url;
                            const poster = filerFile[filerFile.length - 1].url;
                            const alt = filerFile[filerFile.length - 1].fileName;
                            insertFn(url, alt,poster);
                        }
                    };

                };

            },
            //上传前拦截
            onBeforeUpload: (file: File) => {
                let flag = true;
                for (let key in file) {
                    flag = file[key].type.includes('image');
                };
                if (flag) {
                    return file
                } else {
                    return false;
                }
            }
        } : {},
        //上传视频的拦截
        'uploadVideo': {
            onInsertedVideo(videoNode) {
                return message.error('识别错误')
            }
        },
        'codeSelectLang': {
            codeLangs: [
                { text: 'CSS', value: 'css' },
                { text: 'HTML', value: 'html' },
                { text: 'XML', value: 'xml' },
                { text: 'JSON', value: 'json' },
                { test: 'JavaScript', value: 'javascript' },
                { text: 'JAVA', value: 'java' },
            ]
        }
    },
    //告警配置
    customAlert: (s, t) => {
        switch (t) {
            case 'success':
                message.success(s)
                break
            case 'info':
                message.info(s)
                break
            case 'warning':
                message.warning(s)
                break
            case 'error':
                message.error(s)
                break
            default:
                message.info(s)
                break
        }
    },
    //粘贴配置
    customPaste: (editor: IDomEditor, event: any) => {
        const html = event.clipboardData.getData('text/html');
        const text = event.clipboardData.getData('text/plain');
        const rtf = event.clipboardData.getData('text/rtf');

        if (!html && !text) {
            message.error('无法识别复制内容')
            return false;
        };
        return true
    }
};

useEffect(() => {
    //富文本实例
    //@ts-ignore
    const toolbar = DomEditor.getToolbar(editor);
    // console.log(toolbar?.getConfig().toolbarKeys, '默认配置');
    // console.log(editor?.getAllMenuKeys(), '查询编辑器注册的所有菜单key');
    return () => {
        if (editor == null) return;
        editor.destroy()
        setEditor(null)
    }
}, [editor]);

return <>
    <div style={{ border: '1px solid #ccc', zIndex: 100, width: width }}>
        <Toolbar
            editor={editor}

            defaultConfig={toolbarConfig}
            mode="default"
            style={{ borderBottom: '1px solid #ccc' }}
        />
        <Spin spinning={loading}>
            <Editor

                defaultConfig={editorConfig}
                value={html}

                onCreated={setEditor}

                onChange={editor => {
                    const text = editor.getText();
                    const html = editor.getHtml();
                    const isEmpty = editor.isEmpty();
                    if (typeof setCurrentImage === 'function') {
                        setCurrentImage(editor?.getElemsByType('image'));
                    };

                    setHtml(isEmpty ? '' : html);
                    if (onChange && typeof onChange === 'function') {
                        onChange(isEmpty ? '' : html);
                    }
                    // setContentFlag(editor.isEmpty());
                }}

                mode="default"
                style={{ height: height }}
            />
        </Spin>

    </div>
</>

});