wangEditor5问题-复制粘贴图文后图片丢失

2,783 阅读5分钟

问题

从网页上复制图文,粘贴到wangEditor富文本编辑器,图片丢失

image.png

image.png

分析

粘贴板上的html:

<html>
<body>
<!--StartFragment-->
<meta charset="utf-8">
<b style="font-weight:normal;" id="docs-internal-guid-2711f9db-7fff-e625-709d-953484ba76d9">
    <p dir="ltr" style="line-height:1.2;text-align: justify;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:12pt;font-family:'Times New Roman';color:#000000;background-color:transparent;font-weight:700;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Game: </span><span style="font-size:12pt;font-family:'Times New Roman';color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Real Car Crash Compilation</span></p><p dir="ltr" style="line-height:1.2;text-align: justify;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:12pt;font-family:'Times New Roman';color:#000000;background-color:transparent;font-weight:700;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Genre: </span><span style="font-size:12pt;font-family:'Times New Roman';color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Simulation</span></p><p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:12pt;font-family:'Times New Roman';color:#000000;background-color:transparent;font-weight:700;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Package Link: </span><span style="font-size:12pt;font-family:'Times New Roman';color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">https://play.google.com/store/apps/details?id=com.crash.car.mgt&amp;hl=en&amp;gl=US</span></p><p dir="ltr" style="line-height:1.2;text-align: justify;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:12pt;font-family:'Times New Roman';color:#000000;background-color:transparent;font-weight:700;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Video Link: -</span></p><p dir="ltr" style="line-height:1.2;text-align: justify;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:12pt;font-family:'Times New Roman';color:#000000;background-color:transparent;font-weight:700;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Video Time: -</span></p><p dir="ltr" style="line-height:1.2;text-align: justify;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:12pt;font-family:'Times New Roman';color:#000000;background-color:transparent;font-weight:700;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Content Description: </span><span style="font-size:12pt;font-family:'Times New Roman';color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Insane Car Crash!</span></p><p dir="ltr" style="line-height:1.2;text-align: justify;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:12pt;font-family:'Times New Roman';color:#000000;background-color:transparent;font-weight:700;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Title: </span><span style="font-size:12pt;font-family:'Times New Roman';color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Buckle Up and Get Ready to Crash Your Super Car!</span></p><br /><p dir="ltr" style="line-height:1.2;text-align: justify;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:12pt;font-family:'Times New Roman';color:#000000;background-color:transparent;font-weight:700;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Cover</span></p><p dir="ltr" style="line-height:1.2;text-align: justify;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:12pt;font-family:'Times New Roman';color:#000000;background-color:transparent;font-weight:700;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;"><span style="border:none;display:inline-block;overflow:hidden;width:602px;height:338px;"><img src="https://lh5.googleusercontent.com/RauJlNS3tO-i3XpTXQ23lLKWul0qdFXiEd_8Q7Fyj6E34UIomZQt_pwv70pYRiKrJOUg_FLy4adRyR2pjTc5hB0n06SLPEmAoy-6kpu1LtgpwZxNUHlmVEJHrYTdsOAOHO2gmFvtE8vnBJs" width="602" height="338" style="margin-left:0px;margin-top:0px;" /></span></span><span style="font-size:12pt;font-family:'Times New Roman';color:#000000;background-color:transparent;font-weight:700;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;"><span style="border:none;display:inline-block;overflow:hidden;width:602px;height:338px;"><img src="https://lh5.googleusercontent.com/rWz5SrY8vFob9Hb9FbE1ahQUGOwFo3BIOb1gKputxih9nsQ6tT1OgXLw5pkqinMoulQh4YvlZaiaYQdlxpelTtKCeoPpF6sGg6aSd1K8XKCc-ZB9djXFKjkP0NTPh1a_y3HSax6XAShTNxE" width="602" height="338" style="margin-left:0px;margin-top:0px;" /></span></span></p><br /><p dir="ltr" style="line-height:1.2;text-align: justify;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:12pt;font-family:'Times New Roman';color:#000000;background-color:transparent;font-weight:700;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Highlight</span></p>
</b>
<br class="Apple-interchange-newline">
<!--EndFragment-->
</body>
</html>

wangEditor富文本编辑器渲染后:

image.png 如上可知,源html b标签内的文本被全部抽取渲染在span内

寻求帮助

1、github issue

wangEditor5 常见问题汇总

image.png 需要调用方自己实现,未查询到有已实现的范例!

作者有对这个问题做说明: 开源编辑器 wangEditor5 发布两个月总结(官网诚接广告,攒钱租服务器)

2、搜索引擎

wangEditor粘贴从word复制的带图片内容的最佳实践

image.png

解决从word复制粘贴图片丢失的问题,但原因和我的不一样,这里是因为图片标签和内容分离,只粘贴了标签。最终还是调用的dangerouslyInsertHtml

查看源码

1、监听粘贴事件

image.png

image.png

2、insertData

image.png

粘贴最终调用的还是dangerouslyInsertHtml

3、dangerouslyInsertHtml

image.png

源html主要的内容都放在了b标签内,我们跟踪dangerouslyInsertHtml,看b标签会走到哪

image.png

b被作为文本标签 image.png

4、parseElemHtml

image.png

5、 parseTextElemHtml

image.png 由上可知,会将b标签内的文本都取出,生成一个text node

从以上分析可知,问题的原因在于,复制的图文是被b标签包裹的,而b被wangEditor视为文本标签,只取其中的文本渲染。

解决

1、重写dangerouslyInsertHtml

wangEditor无法兼容所有的 HTML 格式,这一点官方文档有特别标红说明。也就是说,我们在编辑器输入内容时,wangEdior 会做一些处理(过滤,筛选,转换等)。

作者是因为html格式非常灵活,不可能全部支持!那我们调用方想要支持各种粘贴的场景,实现起来首先得了解wangEditor的dom -> slate node的转换规则和类型,然后扩展。
这需要对wangEditor的实现原理有深入的了解,且听下回分解。

2、 改造源html的dom结构,使其适配wangEditor的转换规则

/**
 * 遍历html节点,将TEXT_TAGS节点且包含图片子节点的替换成p
 * @param html 
 */
 const replaceTextNodeToBlock = (html: string) => {
    if(!html) {
        return '';
    }

    const div = document.createElement('div');
    div.innerHTML = html;
    div.setAttribute('hidden', 'true');
    document.body.appendChild(div);

    const imgs = div.querySelectorAll('img');
    if(!imgs || !imgs.length) {
        document.body.removeChild(div);
        return html;
    }

    [...imgs].forEach(img => {
        let parentN = img.parentNode;
        while(parentN && parentN !== div) {
            if(TEXT_TAGS.includes(parentN.nodeName.toLowerCase())) {
                const p = document.createElement('p');
                const { innerHTML, attributes } = (<Element>parentN);
                p.innerHTML = innerHTML;
                if(attributes && attributes.length) {
                    console.log('attributes: ', attributes);
                    // eslint-disable-next-line no-restricted-syntax
                    for(const attribute of attributes) {
                        p.setAttribute(attribute.nodeName, attribute.nodeValue as string);
                    }
                }
                parentN.parentNode?.replaceChild(p, parentN);
                parentN = p.parentNode;
            } else {
                parentN = parentN.parentNode;
            }
        }
    });

    const returnHtml = div.innerHTML;
    document.body.removeChild(div);
    return returnHtml;
};

/**
 * 自定义粘贴。可阻止编辑器的默认粘贴,实现自己的粘贴逻辑。
 * @param editor 
 * @param event 
 * @returns 
 */
export const handleCustomPaste = (editor: IDomEditor, event: ClipboardEvent): boolean => {
    console.log('handleCustomPaste: ', editor);

    // 获取粘贴的html部分(??没错粘贴word时候,一部分内容就是html),该部分包含了图片img标签
    let html = (event.clipboardData as DataTransfer).getData('text/html');
    console.log('text/html: ', html);

    if(!html) {
        return true;
    }

    // 获取rtf数据(从word、wps复制粘贴时有),复制粘贴过程中图片的数据就保存在rtf中
    const rtf = (event.clipboardData as DataTransfer).getData('text/rtf');
    console.log('text/rtf: ', rtf);

    // 从html内容中查找粘贴内容中是否有图片元素,并返回img标签的属性src值的集合
    const imgSrcs = findAllImgSrcsFromHtml(html);

    // 没有图片
    if(!imgSrcs || !Array.isArray(imgSrcs) || !imgSrcs.length) {
        return true;
    }

    if(rtf) { // 该条件分支即表示要自定义word粘贴
        // 列表缩进会超出边框,直接过滤掉
        html = html.replace(/text-indent:-(.*?)pt/gi, '');
        // 从rtf内容中查找图片数据
        const rtfImageData = extractImageDataFromRtf(rtf);
        // 如果找到
        if (rtfImageData.length) {
            // TODO:此处可以将图片上传到自己的服务器上
            // 执行替换:将html内容中的img标签的src替换成ref中的图片数据,如果上面上传了则为图片路径
            html = replaceImagesFileSourceWithInlineRepresentation(html, imgSrcs, rtfImageData)
            console.log('result: ', html);
            editor.dangerouslyInsertHtml(html);

            // 阻止默认的粘贴行为
            event.preventDefault();
            return false;
        }
    } else {
        const newHtml = replaceTextNodeToBlock(html);
        console.log('处理深层次嵌套图片: ', newHtml);
        editor.dangerouslyInsertHtml(newHtml);

        // 阻止默认的粘贴行为
        event.preventDefault();
        return false;
    }

    return true;
};

引申其他问题 - office复制的图片还是会丢失,wps不会

对比两者复制出的html内容 image.png 左侧为office,右侧为wps

经调试发现问题出在没识别出office的html中包含img标签,原来是正则的问题,office的html会包含换行
于是将源html去掉换行再进行正则匹配:

/**
 * 从html代码中匹配返回图片标签img的属性src的值的集合
 * @param htmlData
 * @return Array
 */
const findAllImgSrcsFromHtml = (htmlData: string) =>  {
    const imgReg = /<img.*?(?:>|\/>)/gi; // 匹配图片中的img标签
    const arr = htmlData.replace(/[\r\n]/g, ' ').match(imgReg); // 筛选出所有的img
    if (!arr || (Array.isArray(arr) && !arr.length)) {
        return false;
    }
    const srcArr = [];
    const srcReg = /src=['"]?([^'"]*)['"]?/i; // 匹配图片中的src
    for (let i = 0; i < arr.length; i++) {
        const src = arr[i].match(srcReg);
        // 获取图片地址
        src && src.length && srcArr.push(src[1]);
    }
    return srcArr;
}

但是,图片还是没出现!原因是替换src后的图片还是嵌套在文本标签内,需要做replaceTextNodeToBlock处理:

/**
 * 自定义粘贴。可阻止编辑器的默认粘贴,实现自己的粘贴逻辑。
 * @param editor 
 * @param event 
 * @returns 
 */
export const handleCustomPaste = (editor: IDomEditor, event: ClipboardEvent): boolean => {
    console.log('handleCustomPaste: ', editor);

    // 获取粘贴的html部分(??没错粘贴word时候,一部分内容就是html),该部分包含了图片img标签
    let html = (event.clipboardData as DataTransfer).getData('text/html');
    console.log('text/html: ', html);

    if(!html) {
        return true;
    }

    // 获取rtf数据(从word、wps复制粘贴时有),复制粘贴过程中图片的数据就保存在rtf中
    const rtf = (event.clipboardData as DataTransfer).getData('text/rtf');
    console.log('text/rtf: ', rtf);

    // 从html内容中查找粘贴内容中是否有图片元素,并返回img标签的属性src值的集合
    const imgSrcs = findAllImgSrcsFromHtml(html);

    // 没有图片
    if(!imgSrcs || !Array.isArray(imgSrcs) || !imgSrcs.length) {
        return true;
    }

    if(rtf) { // 该条件分支即表示要自定义word粘贴
        // 列表缩进会超出边框,直接过滤掉
        html = html.replace(/text-indent:-(.*?)pt/gi, '');
        // 从rtf内容中查找图片数据
        const rtfImageData = extractImageDataFromRtf(rtf);
        // 如果找到
        if (rtfImageData.length) {
            // TODO:此处可以将图片上传到自己的服务器上
            // 执行替换:将html内容中的img标签的src替换成ref中的图片数据,如果上面上传了则为图片路径
            html = replaceImagesFileSourceWithInlineRepresentation(html, imgSrcs, rtfImageData)
            console.log('result: ', html);
            editor.dangerouslyInsertHtml(html);

            // 阻止默认的粘贴行为
            event.preventDefault();
            return false;
        }
    } else {
        const newHtml = replaceTextNodeToBlock(html);
        console.log('处理深层次嵌套图片: ', newHtml);
        editor.dangerouslyInsertHtml(newHtml);

        // 阻止默认的粘贴行为
        event.preventDefault();
        return false;
    }

    return true;
};