Simple-editor 构成记

Simple-editor: 基于javascript和css开发的 Web富文本编辑器,轻量、简洁、无依赖。
  Github地址

<!DOCTYPE HTML>
<html>
    <head> 
        <meta charset="UTF-8"> 
        <title>Simple-editor demo</title> 
        <!-- css样式文件,webpack加载可忽略 --> 
        <link rel="stylesheet" href="./lib/css/editor.min.css">
    </head>
    <body> 
        <!-- 加载编辑器的容器 --> 
        <div id="container"> 这里写你的初始化内容 </div> 
        <!-- 编辑器源码文件 --> 
        <script type="text/javascript" src="lib/js/editor.min.js"></script> 
        <!-- 实例化编辑器 --> 
        <script type="text/javascript"> 
            var editor = Edit.getEditor('container', {
                ... // 配置参数 
            }); 
        </script>
    </body>
</html>
Simple-editor 展示

因公司业务需求,硬是逼着自己写了个Web端的富文本编辑器,原本用的ueditor,但当页面渲染过多实例后,就显得有些吃力了,毕竟仅仅是压缩过的主文件都有500k,实在有些笨重,再加上项目使用中的一些个性化需求,不得不改动其大片源码,以至于不便于后期迁移和升级维护。
  当然,虽然ueditor近3万行代码,显得很笨重,但其稳建的基础结构还是很值得参考的,所以最终拿了它做主要参考对象。
</br>
  代码主体有四大模块:Edit.utils(工具)、Edit.ui(UI)、Edit.Editor(实例)和Edit.plugin(拓展),以及最底层的Events(事件)。

// 底层事件模块
Events = function(){}
Events.prototype = {}

// 工具类
Edit.utils = {}

// UI类
Edit.ui = {}
// ui 公用方法
Edit.ui.Stateful = {}
// ui Button 构造方法(toolbar工具按钮)
Edit.ui.Button = function(options){}
Edit.ui.Button.prototype = {}
// ui Dialog 构造方法(弹出式选项框)
Edit.ui.Dialog = function(options){}
Edit.ui.Dialog.prototype = {}
// ui Popup 构造方法(次级弹出层)
Edit.Popup = function(options){}
Edit.Popup.prototype = {}

// 拓展方法
Edit.plugin = function(){}

// 编辑器实例
Edit.Editor = function(opt){}
Edit.Editor.prototype = {}

// 创建编辑器实例
Edit.getEdItor = function(){}
// 销毁编辑器实例
Edit.delEditor = function(){}
// 注册UI
Edit.registerUI = function(){}

// 命令及UI创建
Edit.ui['bold'] = function(editor){
    editor.commands['bold'] = {
        execCommand: function(){
            this.document.execCommand('bold', flase, null);
        },
        queryCommandState: function(){
            return this.document.queryCommandState('bold');
        }
    };
    var btn = new Edit.ui.Button({
        name: 'bold',
        className: 'eicon-bold',
        title: editor.options.lang['bold'],
        handles: {
            click: function(){
                editor.execCommand('bold');
            }
        }
    });
    editor.addListener('selectionchange', function(){
        var state = editor.queryCommandState('bold');
        if (!state){
            btn.setChecked(false);
        } else {
            btn.setChecked(true);
        }
    });
    return btn;
}
Edit.ui['xxx'] = function(editor){xxx}
......

主要结构如上,也对其某些环节进行了改良,比如DOM的字符串拼接改成了js虚拟构建,性能方面得到了提升,而且前端页面展示出来的代码也会特别干净;编辑区域方面,考虑到css的局域污染,暂时采用了iframe的嵌套方式,利用其沙盒机制可以有效防止污染(后续使用中如果发现有性能问题,会考虑去掉这种方式,全部放在当前页面操作);
  编辑命令上ueditor有一套自己的封装,这里并没有采用,而是使用了Web标准的编辑API(兼容性方面或许会有些问题,待检测:不考虑远古浏览器),编辑命令主体上是放在ui回调里面注册的,只有在实例化ui的时候才会去进行注册(根据toolbar的配置去注册其对应命令,通用命令'inserthtml'除外);
</br>

// ui 注册
Edit.registerUI('button', function(editor, uiName) {
    //注册按钮名称对应的command命令 
    editor.registerCommand(uiName, { 
        execCommand: function() { alert('execCommand:' + uiName) } 
    }); 
    //创建一个button 
    var btn = new UE.ui.Button({ 
        //按钮的名字 
        name: uiName, 
        //提示 
        title: uiName, 
        //添加额外样式,直接作用于dom元素的style属性
        stlyle: 'background-image:ulr(xxx.png);background-position: -500px 0;', 
        //事件对象,会将对象集合依次遍历,注册在其对应的dom元素上 
        handles: { 
            click: function() { 
                // 这里可以不用执行命令,做自己的操作也可 
                editor.execCommand(uiName); 
            } 
        } 
    }); 
    //当点到编辑内容上时,按钮要做的状态反射 
    editor.addListener('selectionchange', function() { 
        var state = editor.queryCommandState(uiName); 
        if (!state) { 
            btn.setChecked(false); 
        } else { 
            btn.setChecked(state); 
        } 
    }); 
    //因为添加的是button,所以需要返回这个button 
    return btn; 
});

UI注册基本上延续了ueditor的风格,只是属性值有些变化,回调函数中传入了实例对象,爱干嘛干嘛; 可用此方法来开发编辑器插件,比如plugins目录中的mathtype插件就是用此方法实现。
</br>

// 插件方法 拓展
Edit.plugin.register('autouplod', function(){
    function getPasteImage(e){
        return e.clipboardData && e.clipboardData.items && e.clipboardData.items.length == 1 && /^image\//.test(e.clipboardData.items[0].type) ? e.clipboardData.items:null;
    }
    function getDropImage(e){
        return  e.dataTransfer && e.dataTransfer.files ? e.dataTransfer.files:null;
    }

    function sendAndInsertFile(file,editor) {
        var url = editor.options.serverBase64Url;
        if (url) {
            var Form = new FormData();
            var loadingId = 'loading_' + (+new Date()).toString(36);
            Form.append('data', file);
            editor.execCommand('inserthtml', '<img id="'+ loadingId +'" src="/lib/images/loading.gif" style="max-width:100%;height:auto;">');
            
            Edit.Ajax(url,'post',Form,function(cb){
                var loader = editor.document.getElementById(loadingId);
                loader.setAttribute('src',cb.data.url);
                loader.removeAttribute('id');
                Edit.ui.closePopup();
            });
        } else {
            editor.execCommand('inserthtml', '<img style="max-width:100%;height:auto;" src="'+ file +'">');
        }
    }

    return {
            bindEvents:{
            //插入粘贴板的图片,拖放插入图片
            'ready':function(e){
                var self = this;
                if(window.FormData && window.FileReader) {
                    self.bind(self.body, 'paste drop', function(e){
                        var hasImg = false,
                            items;
                        //获取粘贴板文件列表或者拖放文件列表
                        items = e.type == 'paste' ? getPasteImage(e):getDropImage(e);
                        if(items){
                            var len = items.length,
                                file;
                            while (len--){
                                file = items[len];
                                if(file.getAsFile) file = file.getAsFile();
                                if(file && file.size > 0) {                                     
                                    hasImg = true;
                                    var reader = new FileReader();
                                    reader.onload = function (event) {
                                        var base64_str = event.target.result;
                                        sendAndInsertFile(base64_str,self);
                                    }
                                    reader.readAsDataURL(file);  
                                }
                            }
                            hasImg && e.preventDefault();
                        }

                    });
                    //取消拖放图片时出现的文字光标位置提示
                    self.bind(self.body, 'dragover', function (e) {
                        if(e.dataTransfer.types[0] == 'Files') {
                            e.preventDefault();
                        }
                    });
                    
                }
            }
        }
    }
});

插件这部分,可以用它来拓展命令和事件,上面展示的是“图片自动粘贴”。
</br>
  基础介绍如上,详细的实现可能就要去看源码了,虽然有很多现成的Web编辑器可以直接使用,码界也并不提倡重复造轮子,但总是拿来主义也不太好吧,有些坑真的值得去踩一踩、填一填,不然怎好意思自居“开发者”呢,顶多算是个使用者罢了……,当然,更重要的是得满足业务需求。
</br>
  (目前 Simple-editor 主文件代码1600行,压缩后的min版仅30k;好好学习,天天向上,再见!)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容