Bootstrap

百度富文本ueditor实现导入word并将内容显示到编辑器中

当前功能基于PHP,其它语言流程大抵相同。
大概流程:
1. 将docx文件上传到服务器中
2. 使用PHPoffice/PHPword实现将word转换为HTML
3. 将HTML代码返回并赋值到编辑器中

1 编辑器配置修改

1.1 新增上传word json配置

在ueditor\php\config.json中新增如下配置:

    /* 上传word配置 */
    "wordActionName": "wordupload", /* 执行上传视频的action名称 */
    "wordFieldName": "upfile", /* 提交的视频表单名称 */
    "wordPathFormat": "/public/uploads/word/{yyyy}{mm}{dd}/{time}{rand:6}", /* 上传保存路径,可以自定义保存路径和文件名格式 */
    "wordMaxSize": 102400000, /* 上传大小限制,单位B,默认100MB */
    "wordAllowFiles": [".docx"] /* 仅支持docx格式的word */

1.2 修改编辑器配置文件

1.2.1 在工具栏上新增按钮

在ueditor\ueditor.config.js文件中,新增按钮名称"wordupload",并添加鼠标悬浮提示,如下所示:

        //工具栏上的所有的功能按钮和下拉框,可以在new编辑器的实例时选择自己需要的重新定义
        , toolbars: [[
            'fullscreen', 'source', '|', 'undo', 'redo', '|',
            'bold', 'italic', 'underline', 'fontborder', 'strikethrough', 'superscript', 'subscript', 'removeformat', 'formatmatch', 'autotypeset', 'blockquote', 'pasteplain', '|', 'forecolor', 'backcolor', 'insertorderedlist', 'insertunorderedlist', 'selectall', 'cleardoc', '|',
            'rowspacingtop', 'rowspacingbottom', 'lineheight', '|',
            'customstyle', 'paragraph', 'fontfamily', 'fontsize', '|',
            'directionalityltr', 'directionalityrtl', 'indent', '|',
            'justifyleft', 'justifycenter', 'justifyright', 'justifyjustify', '|', 'touppercase', 'tolowercase', '|',
            'link', 'unlink', 'anchor', '|', 'imagenone', 'imageleft', 'imageright', 'imagecenter', '|',
            'simpleupload', 'insertimage', 'emotion', 'scrawl', 'insertvideo', 'music', 'attachment', 'map', 'gmap', 'insertframe', 'insertcode', 'webapp', 'pagebreak', 'template', 'background', '|',
            'horizontal', 'date', 'time', 'spechars', 'snapscreen', 'wordimage', '|',
            'inserttable', 'deletetable', 'insertparagraphbeforetable', 'insertrow', 'deleterow', 'insertcol', 'deletecol', 'mergecells', 'mergeright', 'mergedown', 'splittocells', 'splittorows', 'splittocols', 'charts', '|',
            'print', 'preview', 'searchreplace', 'drafts', 'help', 'wordupload'
        ]]
        //当鼠标放在工具栏上时显示的tooltip提示,留空支持自动多语言配置,否则以配置值为准
        ,labelMap:{
           'wordupload': '上传word文件',
        }

在ueditor\themes\default\images\目录下新增按钮图标"word_upload.png":
导入图标
在ueditor\themes\default\css\ueditor.css文件中新增按钮样式:

.edui-for-wordupload .edui-icon {
    width: 16px;
    height: 16px;
    background: url(../images/word_upload.png) no-repeat 2px 2px !important;
}

最后在ueditor\ueditor.all.js文件中editorui["simpleupload"] = function (editor){}后面添加如下代码:

/* word上传 */
    editorui["wordupload"] = function (editor) {
        var name = 'wordupload',
            ui = new editorui.Button({
                className:'edui-for-' + name,
                title:editor.options.labelMap[name] || editor.getLang("labelMap." + name) || '',
                onclick:function () {},
                theme:editor.options.theme,
                showText:false
            });
        editorui.buttons[name] = ui;
        editor.addListener('ready', function() {
            var b = ui.getDom('body'),
                iconSpan = b.children[0];
            editor.fireEvent('worduploadbtnready', iconSpan);
        });
        editor.addListener('selectionchange', function (type, causeByUi, uiReady) {
            var state = editor.queryCommandState(name);
            if (state == -1) {
                ui.setDisabled(true);
                ui.setChecked(false);
            } else {
                if (!uiReady) {
                    ui.setDisabled(false);
                    ui.setChecked(state);
                }
            }
        });
        return ui;
    };

最终样式如下:
示例图1

1.2.2 新增语言配置

在ueditor\lang\zh-cn\zh-cn.js文件中在"simpleupload"配置下方新增以下配置:

	'simpleupload':{
        'exceedSizeError': '文件大小超出限制',
        'exceedTypeError': '文件格式不允许',
        'jsonEncodeError': '服务器返回格式错误',
        'loading':"正在上传...",
        'loadError':"上传错误",
        'errorLoadConfig': '后端配置项没有正常加载,上传插件不能正常使用!'
    },
    'wordupload':{
        'exceedSizeError': '文件大小超出限制',
        'exceedTypeError': '文件格式不允许',
        'jsonEncodeError': '服务器返回格式错误',
        'loading':"正在上传...",
        'loadError':"上传错误",
        'errorLoadConfig': '后端配置项没有正常加载,上传插件不能正常使用!'
    },

在ueditor\lang\zh-cn\en.js文件中在"simpleupload"配置下方新增以下配置:

	'simpleupload':{
        'exceedSizeError': 'File Size Exceed',
        'exceedTypeError': 'File Type Not Allow',
        'jsonEncodeError': 'Server Return Format Error',
        'loading':"loading...",
        'loadError':"load error",
        'errorLoadConfig': 'Server config not loaded, upload can not work.',
    },
    'wordupload':{
        'exceedSizeError': 'File Size Exceed',
        'exceedTypeError': 'File Type Not Allow',
        'jsonEncodeError': 'Server Return Format Error',
        'loading':"loading...",
        'loadError':"load error",
        'errorLoadConfig': 'Server config not loaded, upload can not work.',
    },

1.2.3 修改过滤配置

由于导入word时,编辑器会自动过滤掉图片等样式,所以需取消过滤
在ueditor\ueditor.config.js文件中修改如下配置:

		// xss 过滤是否开启,inserthtml等操作
		,xssFilterRules: false
		//input xss过滤
		,inputXssFilter: false
		//output xss过滤
		,outputXssFilter: false

在ueditor\ueditor.all.js文件中,修改UE.plugins[‘defaultfilter’],新增return ;如下所示:

// plugins/defaultfilter.js
///import core
///plugin 编辑器默认的过滤转换机制

UE.plugins['defaultfilter'] = function () {
    return;
    var me = this;
    me.setOpt({
        'allowDivTransToP':true,
        'disabledTableInTable':true
    });
    ……

2 添加相关功能

2.1 安装PHPword

composer require phpoffice/phpword

2.2 自定义文件转换类

实现上传文件,并将文件转换为HTML
直接将ueditor自带的上传文件"ueditor\php\Uploader.class.php"类复制到自定义类相同目录下
自定义WordToHtmlController.class.php文件:

<?php

class WordToHtmlController
{
    public function index()
    {
        require 'vendor/autoload.php';

        $base64 = "upload";

        $config = array(
            "pathFormat" => '/public/uploads/word/{yyyy}{mm}{dd}/{time}{rand:6}',
            "maxSize" => 102400000,
            "allowFiles" => [".docx"]
        );
        $fieldName = 'upfile';

        include 'Uploader.class.php';
        $up = new Uploader($fieldName, $config, $base64);
        $path = ltrim($up->getFileInfo()['url'], '/');

//        $phpWord = \PhpOffice\PhpWord\IOFactory::load('public/uploads/word/20211029/test.docx');
        $phpWord = \PhpOffice\PhpWord\IOFactory::load($path);

        // 直接输出到页面显示
//        $phpWord->save('php://output', 'HTML');

        $xmlWriter = \PhpOffice\PhpWord\IOFactory::createWriter($phpWord, 'HTML');

        header("Content-Type:text/html; charset=utf-8");
        exit($this->replaceImageSrc($xmlWriter->getContent()));
//        exit($xmlWriter->getContent());
    }

    /**
     * 将HTML代码中的所有图片地址替换
     * @param $content string 要查找的内容
     * @return string
     */
    private function replaceImageSrc($content)
    {
        $preg = '/(\s+src\s?\=)\s?[\'|"]([^\'|"]*)/is'; // 匹配img标签的正则表达式
        preg_match_all($preg, $content, $allImg); // 匹配所有的img

        if (!$allImg)
            return $content;

        foreach ($allImg[0] as $k => $v) {
            $old = ltrim($v, '" src=');

            preg_match('/^(data:\s*image\/(\w+);base64,)/', $old, $temp);
            $tempType = $temp[2];   // 获取类型

            // 判断目录是否存在,不存在时创建
            $tempFilePath = 'public/uploads/word_images/' . date('Y-m-d', time());
            if (!file_exists($tempFilePath))
                mkdir($tempFilePath);

            // 拼接完整路径
            $tempFileName = $tempFilePath . '/word_image_' . time() . $k . '.' . $tempType;
            $base64 = str_replace($temp[1], '', $old);

            file_put_contents($tempFileName, base64_decode($base64));

            // 替换路径字符串
            $content = str_replace($old, $tempFileName, $content);
        }
        return $content;
    }
}


2.3 编辑器实现导入操作

在ueditor\ueditor.all.js文件中UE.plugin.register('simpleupload', function (){})下方新增如下方法:


/**
 * @description
 * word上传:点击按钮,直接选择文件上传
 */
UE.plugin.register('wordupload', function (){
    var me = this,
        isLoaded = false,
        containerBtn;

    function initUploadBtn(){
        var w = containerBtn.offsetWidth || 20,
            h = containerBtn.offsetHeight || 20,
            btnIframe = document.createElement('iframe'),
            btnStyle = 'display:block;width:' + w + 'px;height:' + h + 'px;overflow:hidden;border:0;margin:0;padding:0;position:absolute;top:0;left:0;filter:alpha(opacity=0);-moz-opacity:0;-khtml-opacity: 0;opacity: 0;cursor:pointer;';

        domUtils.on(btnIframe, 'load', function(){

            var timestrap = (+new Date()).toString(36),
                wrapper,
                btnIframeDoc,
                btnIframeBody;

            btnIframeDoc = (btnIframe.contentDocument || btnIframe.contentWindow.document);
            btnIframeBody = btnIframeDoc.body;
            wrapper = btnIframeDoc.createElement('div');

            wrapper.innerHTML = '<form id="edui_form_' + timestrap + '" target="edui_iframe_' + timestrap + '" method="POST" enctype="multipart/form-data" action="' + me.getOpt('serverUrl') + '" ' +
                'style="' + btnStyle + '">' +
                '<input id="edui_input_' + timestrap + '" type="file" accept="application/msword" name="' + me.options.wordFieldName + '" ' +
                'style="' + btnStyle + '">' +
                '</form>' +
                '<iframe id="edui_iframe_' + timestrap + '" name="edui_iframe_' + timestrap + '" style="display:none;width:0;height:0;border:0;margin:0;padding:0;position:absolute;"></iframe>';

            wrapper.className = 'edui-' + me.options.theme;
            wrapper.id = me.ui.id + '_iframeupload';
            btnIframeBody.style.cssText = btnStyle;
            btnIframeBody.style.width = w + 'px';
            btnIframeBody.style.height = h + 'px';
            btnIframeBody.appendChild(wrapper);

            if (btnIframeBody.parentNode) {
                btnIframeBody.parentNode.style.width = w + 'px';
                btnIframeBody.parentNode.style.height = w + 'px';
            }

            var form = btnIframeDoc.getElementById('edui_form_' + timestrap);
            var input = btnIframeDoc.getElementById('edui_input_' + timestrap);
            var iframe = btnIframeDoc.getElementById('edui_iframe_' + timestrap);

            domUtils.on(input, 'change', function(){
                if(!input.value) return;
                var loadingId = 'loading_' + (+new Date()).toString(36);
                var allowFiles = me.getOpt('wordAllowFiles');

                me.focus();
                me.execCommand('inserthtml', '<img class="loadingclass" id="' + loadingId + '" src="' + me.options.themePath + me.options.theme +'/images/spacer.gif" title="' + (me.getLang('wordupload.loading') || '') + '" >');

                function callback(){
                    try{
                        // 获取到内容
                        var body = (iframe.contentDocument || iframe.contentWindow.document).body;

                        // 获取加载中图片并关闭
                        var loader = me.document.getElementById(loadingId);
                        loader.removeAttribute('id');
                        domUtils.removeClasses(loader, 'loadingclass');

                        // 向编辑器赋值
                        me.setContent(body.innerHTML, false);
                        // me.execCommand('insertHtml', body.innerHTML);
                    }catch(er){
                        showErrorLoader && showErrorLoader(me.getLang('wordupload.loadError'));
                    }
                    form.reset();
                    domUtils.un(iframe, 'load', callback);
                }
                function showErrorLoader(title){
                    if(loadingId) {
                        var loader = me.document.getElementById(loadingId);
                        loader && domUtils.remove(loader);
                        me.fireEvent('showmessage', {
                            'id': loadingId,
                            'content': title,
                            'type': 'error',
                            'timeout': 4000
                        });
                    }
                }

                /* 判断后端配置是否没有加载成功 */
                if (!me.getOpt('wordActionName')) {
                    errorHandler(me.getLang('autoupload.errorLoadConfig'));
                    return;
                }
                // 判断文件格式是否错误
                var filename = input.value,
                    fileext = filename ? filename.substr(filename.lastIndexOf('.')):'';
                if (!fileext || (allowFiles && (allowFiles.join('') + '.').indexOf(fileext.toLowerCase() + '.') == -1)) {
                    showErrorLoader(me.getLang('wordupload.exceedTypeError'));
                    return;
                }

                domUtils.on(iframe, 'load', callback);

                // 上传操作
                // form.action = utils.formatUrl(imageActionUrl + (imageActionUrl.indexOf('?') == -1 ? '?':'&') + params);
                // 替换请求地址为框架后台地址
                form.action = "/admin.php?m=fwordToHtml&a=index"
                form.method = "post"
                form.submit();
            });

            var stateTimer;
            me.addListener('selectionchange', function () {
                clearTimeout(stateTimer);
                stateTimer = setTimeout(function() {
                    var state = me.queryCommandState('wordupload');
                    if (state == -1) {
                        input.disabled = 'disabled';
                    } else {
                        input.disabled = false;
                    }
                }, 400);
            });
            isLoaded = true;
        });

        btnIframe.style.cssText = btnStyle;
        containerBtn.appendChild(btnIframe);
    }

    return {
        bindEvents:{
            'ready': function() {
                //设置loading的样式
                utils.cssRule('loading',
                    '.loadingclass{display:inline-block;cursor:default;background: url(\''
                    + this.options.themePath
                    + this.options.theme +'/images/loading.gif\') no-repeat center center transparent;border:1px solid #cccccc;margin-right:1px;height: 22px;width: 22px;}\n' +
                    '.loaderrorclass{display:inline-block;cursor:default;background: url(\''
                    + this.options.themePath
                    + this.options.theme +'/images/loaderror.png\') no-repeat center center transparent;border:1px solid #cccccc;margin-right:1px;height: 22px;width: 22px;' +
                    '}',
                    this.document);
            },
            /* 初始化word上传按钮 */
            'worduploadbtnready': function(type, container) {
                containerBtn = container;
                me.afterConfigReady(initUploadBtn);
            }
        },
        outputRule: function(root){
            utils.each(root.getNodesByTagName('img'),function(n){
                if (/\b(loaderrorclass)|(bloaderrorclass)\b/.test(n.getAttr('class'))) {
                    n.parentNode.removeChild(n);
                }
            });
        },
        commands: {
            'wordupload': {
                queryCommandState: function () {
                    return isLoaded ? 0:-1;
                }
            }
        }
    }
});

然后在同文件下的btnCmds变量中添加上自定义的按钮:

    //为工具栏添加按钮,以下都是统一的按钮触发命令,所以写在一起
    var btnCmds = ['undo', 'redo', 'formatmatch',
        'bold', 'italic', 'underline', 'fontborder', 'touppercase', 'tolowercase',
        'strikethrough', 'subscript', 'superscript', 'source', 'indent', 'outdent',
        'blockquote', 'pasteplain', 'pagebreak',
        'selectall', 'print','horizontal', 'removeformat', 'time', 'date', 'unlink',
        'insertparagraphbeforetable', 'insertrow', 'insertcol', 'mergeright', 'mergedown', 'deleterow',
        'deletecol', 'splittorows', 'splittocols', 'splittocells', 'mergecells', 'deletetable', 'drafts', 'wordupload'];

至此,配置完成,结果示意图:
word文件
编辑器示意图
编辑器HTML示意图

;