编程技术文章分享与教程

网站首页 > 技术文章 正文

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

hmc789 2024-11-24 16:49:44 技术文章 2 ℃

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

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

第一次改造:Vue -> React

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

Vue时代

就是图中绿框中的部分。

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

  • 用Vue中编辑签名相应的功能
  • 在wangEditor的源码中将其整体插入

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

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

React时代

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

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

然后用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结构效果对比

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

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

Tags:

标签列表
最新留言