5

2021年的一点工作总结(二)富文本编辑器

 2 years ago
source link: https://www.daozhao.com/10374.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

2021年的一点工作总结(二)富文本编辑器

如果您发现本文排版有问题,可以先点击下面的链接切换至老版进行查看!!!

2021年的一点工作总结(二)富文本编辑器

邮件项目的核心功能就是编辑邮件了,所以文本的编辑特别容易被用户吐槽了。用户报障的时候一个万能的吐槽点“没有xxx功能,不支持xxx,没有Outlook好用”。 其实作为一个web产品,如果需要更加公平的对比的话,应该远程web版网页邮箱对比(如outlook网页版),而不是客服端软件(如Outlook),普通用户不知道也不会这么对比,所以你也只能受着。。。 从原来的wangEditor替换成CKEditor也不是说就万事大吉了,官方什么插件都有,什么功能都不用自己开发了,那你就大错特错了。说去不负责任的话,支持功能越多,开放给用户的就越多,然后被吐槽的就越多。

2021年,因为富文本的缘故我做过几次相关的改造:

第一次改造:Vue -> React

项目从Vue迁移到React,虽然当时并未“动”编辑器,仍用的是老的wangEditor,但是为了邮件的编辑器“插件”签名和快捷回复功能(以下主要以“签名”为例),能跟原来一致的使用体验。

Vue时代

file

就是图中绿框中的部分。

这两个功能一直是用Vue写的,然后集中在一个名为editorChildren的div中,然后该div作为整体被wangEditor编辑器append到指定的dom结构中使用,大致用法如下:

  • 用Vue中编辑签名相应的功能 file

  • 在wangEditor的源码中将其整体插入

file

这里提到的iframe就是在上图中rich-editor.vue给预留的id为editorIframe的iframe,wangEditor之前已经被改造成在iframe中使用了。

按照这种模式可以将签名对应的功能直接用Vue来编写,不用费劲的用原生JS实现了,开发效率更高。

React时代

迁移到React后,发现不能原来这种模式了,这样会导致签名上面绑定的点击事件没有反应,这个应该是跟React对点击事件的响应机制有关。

所以不得不对此做些改动,将上述的editorChildren的那部分直接写成一个React组件ToolBar

file

然后用render方法生成对应的html,再有wangEditor将其插入对应的dom中

import ReactDom from 'react-dom';
import Toolbar from "../components/editor-widgets/Toolbar";

const children = document.querySelector('#editorChildren');
ReactDom.render(<Toolbar/>, tools);
iframe.contentDocument.body.append(children);

至此原来的签名和快捷回复功能算是能在React项目中正常使用了。

第二次改造 wangEditor -> CKEditor

这次算是最大的一次改造了,就是要把wangEditor替换成CKEditor。

CKEditor可不支持之前像wangEditor那样的骚操作了,它是有自己的插件编写规则的,我个人也不想像对带wangEditor那样改源码了。

CKEditor的插件都是用原生JavaScript编写的。

(function () {
    CKEDITOR.plugins.add( 'mailshortcutreply', {
        requires: ['mailcontent', 'mailshortcutreplyrichcombo'],
        lang: 'en,ja,zh-cn,zh,shark', // %REMOVE_LINE_CORE%
        template: '<div class="shortcutreply_container">{1}</div>',
        init: function( editor ) {
            var lang = editor.lang.mailshortcutreply;
            editor.addCommand( 'mailShortcutReplyTree', {
                exec: function( editor, tree ) {
                    CKEDITOR.mail.shortcutReplyTree = tree;
                    editor.ui.instances.ShortcutReply.reset();
                },
                editorFocus: false
            });
            var config = editor.config;

            var panelTitle = lang.label;
            var styles = {};

            editor.ui.addShortcutReplyRichCombo( 'ShortcutReply', {
                label: panelTitle,
                title: panelTitle,
                toolbar: 'styles,20',
                command: 'shortcutReply',
                // allowedContent: allowedContent,

                panel: {
                    css: [ CKEDITOR.skin.getPath( 'editor' ) ].concat( CKEDITOR.skin.getPath( 'wangeditor' ) ),
                    level: 'mailshortcutreply',
                    multiSelect: false,
                    attributes: { 'aria-label': panelTitle }
                },

                init: function() {
                    this.startGroup( panelTitle );
                    var that = this;
                    CKEDITOR.mail.shortcutReplyTree.forEach(function (item) {
                        that.add( item.key, item.title, item.title);
                    });
                    if (CKEDITOR.mail.shortcutReplyTree.length === 0) {
                        // 鉴于length === 0不会调用add方法,故需要手动设置标志位started
                        this._.list._.started = 1;
                    }
                    // 默认选中第一个,先缓存值
                    var firstCategory = CKEDITOR.mail.shortcutReplyTree[0];
                    if (firstCategory) {
                        this.storeInitValue(firstCategory.key);
                    }
                },

                selectedList: function(key, searchText) {
                    var category = CKEDITOR.mail.shortcutReplyTree.find(function (item) {
                        return item.key === key
                    });
                    if (category && category.children && category.children.length > 0) {
                        return category.children.filter(function(item) {
                            var tempDiv = document.createElement('div');
                            tempDiv.innerHTML = item.content;
                            var plainTextContent = tempDiv.innerText;
                            // 根据内容(里面可能有html代码)的纯文字来检索
                            return item.title.includes(searchText) || plainTextContent.includes(searchText);
                        });
                    }
                    return [];
                },
                fireSearch: function() {
                    var inputValue = this.getStoreInputValue();
                    var value = this.getStoreValue();
                    var list = this.selectedList(value, inputValue);
                    var that = this;
                    list.forEach(function (item) {
                        that.addResult( item.key, item.text, item.title);
                    });
                    this.setStoreValue(value);
                    this.commitResult();
                },
                storeInitValue: function(value) {
                    this.setStoreValue(value || '');
                },
                onClick: function( value ) {
                    this.setStoreValue(value);
                    this.fireSearch();
                },
                onSettingClick: function() {
                    $MailMessageCenter.publish('editor.goToSetting', ['shortcutReply']);
                },
                onChange: function(inputValue) {
                    this.setStoreInputValue(inputValue);
                    this.fireSearch();
                },
                onSelect: function(value) {
                    editor.focus();
                    editor.fire( 'saveSnapshot' );
                    var categoryKey = this.getStoreValue();
                    var category = CKEDITOR.mail.shortcutReplyTree.find(function (item) {
                        return item.key === categoryKey;
                    });
                    if (category) {
                        var shortcutReply = (category.children || []).find(function (item) {
                            return item.key === value;
                        });
                        CKEDITOR.mail.insertHtml(shortcutReply.text);
                    }
                },
                onOpen: function() {
                    this.showAll();
                },

                reset: function() {
                    if (this._.committed) { // 已经初始化过的,则需要进行重置
                        this.destroy();
                        this._.panel = void 0;
                        if (this._.list && this._.list.element) {
                            this._.list.element.$.remove();
                        }
                        this._.committed = 0;
                        this.createPanel(editor);
                    }
                },

                refresh: function() {
                    var elementPath = editor.elementPath();

                    if ( !elementPath )
                        return;
                }
            } );
        }
    });
})();

里面的过滤、搜索、选择什么写起来都没有以前那么爽了,这大概也是Vue、React这类框架的意义之一吧。

第三次改造 默认样式dom结构调整

因为CKEditor是支持格式刷功能,虽然部分场景有bug,但是还是决定开放给用户使用,原来的默认样式功能跟格式刷不兼容,导致这部分内容使用格式刷存在bug,所以只好改造了默认样式的dom结构。具体可以参考之前写过的 CKEditor系列(六)改造原编辑器默认样式dom结构效果对比

对于编辑器,有太多的坑,要想满足用户一切省事的需求,有太多的地方需要改造了,只看你觉得值不值了。

【这不是结束,这甚至不是结束的开始。但,这可能是开始的结束。】


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK