Simple-editor 构成记

2016-09-26  本文已影响0人  旅行着的魔法师

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;好好学习,天天向上,再见!)

上一篇下一篇

猜你喜欢

热点阅读