遇到一个问题,就是有个富文本编辑器需要能从 MS Word 那边复制,粘贴内容到编辑器里面,图片要附带过去。

我在网络上测试了几个编辑器,大都不支持,如果全部不支持,我就可以得出结论,说技术上做不到。不过测试之后发现 CKEditortinymce 是支持直接粘贴 Word 文档内容,并且能读取到图片。

后面就是各种收集资料和调试,实现流程并不难,第一步现实监听粘贴事件,首先创建一个编辑器:

<div id="editor" contenteditable="true"></div>
<script>
    const editor = document.querySelector("#editor");
    editor.addEventListener("paste", e => {
        const {clipboardData} = e;
        console.log(clipboardData.types, clipboardData.items);
    });
</script>

监听 paste 事件,触发的时候是 ClipboardEvent 对象,里面包含了 clipboardData是剪贴板内的数据。

  • clipboardData.types 包含了剪贴板里面项目的数据类型,可能是文本或是文件。
  • clipboardData.items 不同类型的数据。
  • clipboardData.getData() 获取指定类型的数据

简单说,如果从网页拷贝一段内容粘贴到编辑器里面,那么 types 会包含两个数据 ["text/html","text/plain"] ,一个是网页源代码,一个是去除标签后的数据。在需要粘贴的时候,目标软件会依据这个类型,寻找出最合适的那个类型数据导入进去。如果你从浏览器地址栏拷贝地址粘贴进去,这时候类型是["text/plain"],只有纯文本数据,没有其他的样式。在系统资源管理器里面,复制一个文件到编辑器粘贴,剪贴板的数据类型就是["Files"],因此网页是可以读取剪贴板里面的文件的。

那么如果是从 Word 里面拷贝一段内容,粘贴到网页里面的上面的编辑框内,获取到的剪贴板数据类型是:["text/plain","text/html","text/rtf"],编辑器不会识别text/rtf 格式,因此会采用text/html 里面的数据,包含的内容是 Word 导出来的完整网页代码,但是图片路径存在错误:

<img src="file:///..." name="image1.png" align="bottom" width="225" height="225" border="0"/>

因此,你会看到除了图片的未知空着之外,其他格式都是正常的。打印剪贴板内容代码如下:

<script>
    const editor = document.querySelector("#editor");
    editor.addEventListener("paste", e => {
        const {clipboardData} = e;
        console.log(clipboardData.types, clipboardData.items);
        clipboardData.types.includes("text/html")
        && console.log(clipboardData.getData("text/html"));
    });
</script>

text/rtf 是微软的专有格式,里面包含了图片的数据,而不是路径信息,因此解析这个内容,然后转换成 text/html 格式或是直接获取到数据插入富文本编辑器,就可以实现从 Word 拷贝图片的功能。

目前的问题,找不到好的 RTF 格式内容解析的库,如果需要对 Word 拷贝内容有比较高的兼容需求,可以考虑自己编写这个解析库。第三方的有:rtf.js

完整的网页代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/WMFJS.bundle.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/EMFJS.bundle.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/RTFJS.bundle.min.js"></script>
    <style>
        html, body {
            padding: 0;
            margin: 0;
        }

        body {

            background-color: #eee;
        }

        #editor {
            background-color: #fff;
            width: 500px;
            height: 200px;
            margin: 100px auto;
            border-radius: 5px;
            padding: 15px;
            font-size: 18px;
            border: red solid 1px;
            overflow-y: auto;
        }
    </style>
</head>
<body>
<div id="editor" contenteditable="true"></div>
<script>

    function stringToArrayBuffer(string) {
        const buffer = new ArrayBuffer(string.length);
        const bufferView = new Uint8Array(buffer);
        for (let i = 0; i < string.length; i++) {
            bufferView[i] = string.charCodeAt(i);
        }
        return buffer;
    }

    const editor = document.querySelector("#editor");
    editor.addEventListener("paste", e => {
        const {clipboardData} = e;
        console.log(clipboardData.types, clipboardData.items);
        if (!clipboardData.types.includes("text/rtf")) return;
        const rtf = clipboardData.getData("text/rtf");
        const doc = new RTFJS.Document(stringToArrayBuffer(rtf));
        const meta = doc.metadata();
        doc.render().then(function (htmlElements) {
            console.log("Meta:");
            console.log(meta);
            console.log("Html:");
            console.log(htmlElements);
            htmlElements.forEach(ele => editor.appendChild(ele));
        }).catch(error => console.error(error))
        e.stopPropagation();
        e.preventDefault();
    });
</script>

</body>
</html>