经过前几节的层层递进,我们了解了一个利用原生的contenteditable 属性和 execCommand 方法来实现一个简单版的富文本编辑器,但是目前版本的编辑器虽然功能上基本实现了,但是作为一个应用或者工程,还很低级,它的部分变量和全部方法暴露在全局变量下,并且无法在同一个页面实例化多个互不干扰的编辑器。

本节我们就利用面向对象方法来对该编辑器进行封装设计。

设计分析

如果每一个编辑器是对象,那么我们所编写的程序一定是一个对象工厂,可以根据传入的配置来生成不同的编辑器实例对象,并且要将生成的编辑器实例插入指定的DOM元素中,生成方式应该类似于下面这种:

var editor = new Editor($el, options);

目前,每一个编辑器实例至少应该包含以下部分:

  • 工具条
  • 编辑区

而为了保证工具条与编辑区不会抢夺焦点,编辑区必须位于一个框架(iframe)内.

而为了完整的封装性,我们需要保证编辑器的各个组件DOM都动态生成,所以我们不能再像前面几节一样,把编辑器的骨架HTML写在html文件中,所以,每个组件都要有自己的UI模板,需要有一个获取模板的方法。

为了更好的扩展性,我们还需要为每一个组件提供显示/隐藏方法。

工具条上的按钮又分以下几种:

  • 直接执行类,如:加粗、斜体等
  • 弹出对话框,填写后执行类: 如添加链接、图片、前景色、背景色等
  • 下拉选择类,如:字体、字号等,这些是需要传入选项数组的。

这里面,我们又可以抽象出一个新的组件:对话框,同样,它也有自己的模板方法,显示/隐藏方法。

当然,我们还需要一个事件系统,用来为组件(按钮)增加事件监听,并且在组件销毁(隐藏)时移除监听。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ak7EHSqM-1670513320945)(https://gitee.com/hjb2722404/tuchuang/raw/master/img/202211170937952.png)]

实现

编辑器类实现

class Editor {

    constructor($el, options) {

        this.id = options.id || this.createId();

        this.$el = $el;

        this.options = options;

        this.init();

    }

    init() {

        this.editorBar = new EditorBar(this.id, this.options);

        this.editorContent = new EditorContent(this.id, this.options);

        this.getTemplate();

        this.show();

        this.initEvents();

    }

    getTemplate() {

        const editorBarTpl = this.editorBar.getTemplate();

        const editorContentTpl = this.editorContent.getTemplate();

        return `<div id="editor-${this.id}" editor>${editorBarTpl}${editorContentTpl}</div>`;

    }

    show() {

        document.getElementById('editor-' + this.id).display = 'block';

    }
    /**

     * [initEvents 初始化事件绑定]

     *

     * @return  {[type]}  [return description]

     */

    initEvents() {

        const btns = document.getElementById(this.editorBar.id).getElementsByTagName('button');

        let btnsArray = [];

        for(let i=0; i<btns.length; i++) {

            btnsArray.push(btns[i]);

        }

        const that = this;

        for (let i=0; i<btnsArray.length; i++) {

            const btn = btnsArray[i];

            const command = btn.getAttribute('command');

            btn.onclick = function(e) {

                switch (command) {

                    case 'fontColor':

                        that.dialog = new editorDialog(that.id);

                        that.colorPicker = new colorPicker(that.id, btn, 'foreColor');

                        that.dialog.setCom(that.colorPicker.getTemplate());

                        that.colorPicker.show();

                        that.dialog.location(command);

                        that.dialog.show();

                        that.colorPicker.initEvents(that);

                        break;

                    case 'backColor':

                        that.dialog = new editorDialog(that.id);

                        that.colorPicker = new colorPicker(that.id, btn, 'backColor');

                        that.dialog.setCom(that.colorPicker.getTemplate());

                        that.colorPicker.show();

                        that.dialog.location(command);

                        that.dialog.show();

                        that.colorPicker.initEvents(that);

                        break;

                    case 'createLink':

                        that.dialog = new editorDialog(that.id);

                        that.linkEditor = new linkEditor(that.id);

                        that.dialog.setCom(that.linkEditor.getTemplate());

                        that.linkEditor.show();

                        that.dialog.location(command);

                        that.dialog.show();

                        that.linkEditor.initEvents(that);

                        break;

                    case 'insertImage':

                        that.dialog = new editorDialog(that.id);

                        that.imageEditor = new imageEditor(that.id);

                        that.dialog.setCom(that.imageEditor.getTemplate());

                        that.imageEditor.show();

                        that.dialog.location(command);

                        that.dialog.show();

                        that.imageEditor.initEvents(that);

                        break;

                    default:

                        that.editor.document.execCommand(command, 'true', '');

                }

            };

        }

        this.editorBar.events.map((event) => {

            let el = document.getElementById(event.id);

            el.addEventListener(event.type, function (e) {

                console.log(event.key);

                console.log(e.target.value);

                that.editor.document.execCommand(event.key, false, e.target.value);

            });

        });

    }
}

编辑器工具条类实现

class EditorBar {

    constructor(id, options) {

        this.id = `editorBar-${id}`;

        this.options = options;

        this.init();

    }

  

    init() {

        this.getTemplate();

    }

  

    show() {

        document.getElementById(this.id).display = 'block';

    }

  

    hide() {

        document.getElementById(this.id).display = 'none';

    }

  

    getTemplate() {

        const $tpl = this.createEditorBar();

        return `<div id="${this.id}" class="editor-toolbar">${$tpl}</div>`;

    }

  

    createEditorBar() {

        let $tpl ='<ul>';

        for (key in this.options.btns) {

            if (commandsMap[key].options) {

                let id = this.id + '-' + key + 'Selector';

                let customStyleName = commandsMap[key].styleName;

                $tpl += getSelectTpl(id, commandsMap[key].options, customStyleName);

            } else {

                $tpl += `<li><button command="${key}"><i class="iconfont icon-${commandsMap[key].icon}" title="${commandsMap[key].title}"></i></button></li>`;

            }

        }

        $tpl += '</ul>';

        return $tpl;

    }

}

编辑器内容区类实现

class EditorContent {

    constructor(id, options) {

        this.id = `editorContent-${id}`;

        this.options = options;

        this.init();

    }

  

    init() {

        this.getTemplate();

    }

  getTemplate() {

        return `<iframe id="${this.id}" class="editor-content" contenteditable="true" frameborder="0"></iframe>`;

    }

    show() {

        document.getElementById(this.id).display = 'block';

    }

  

    hide() {

        document.getElementById(this.id).display = 'none';

    }

}

弹窗容器类

class editorDialog {

    constructor(id) {

        this.id = 'editorDialog-' + id;

        this.editorId = 'editor-' + id

        this.init();

    }

  

    getSelf() {

        return document.getElementById(this.id);

    }

  

    init() {

        const div = document.createElement('div');

        div.innerHTML = this.getTemplate();

        document.getElementById(this.editorId).appendChild(div);

    }

  

    setCom(tpl) {

        document.getElementById(this.id).innerHTML = tpl;

    }

  

    getTemplate() {

        return `<div class="editor-dialog" id="${this.id}"></div>`;

    }

  

    show() {

        document.getElementById(this.id).style.display = 'block';

    }

  

    hide() {

        document.getElementById(this.id).style.display = 'none';

    }

  

    location(cmd) {

        const btns = document.getElementsByClassName(cmd += '-btn');

        if (btns.length) {

            const btn = btns[0];

            const style = {

                top: (btn.offsetTop + btn.offsetHeight + 15) + 'px',

                left: btn.offsetLeft + 'px',

            };

            const dialog = this.getSelf();

            dialog.style.top = style.top;

            dialog.style.left = style.left;

        }

    }

}

颜色选择器类

class colorPicker {

  

    constructor(id, btn, type) {

        this.id = 'colorPicker-' + id;

        this.btn = btn;

        this.type = type;

    }

  

    getSelf() {

        return document.getElementById(this.id);

    }

  

    getTemplate() {

        return `

            <input type="color" id="${this.id}" />

        `;

    }

  

    show() {

        document.getElementById(this.id).display = 'block';

    }

  

    hide() {

        document.getElementById(this.id).display = 'none';

    }

  

    initEvents(editInstance) {

        const colorPicker = this.getSelf();

        const that = this;

        colorPicker.addEventListener("input", function(event){

            editInstance.editor.document.execCommand(that.type, 'false', event.target.value);

            editInstance.dialog.hide();

        }, false);

        colorPicker.addEventListener("change", function(event){

            editInstance.editor.document.execCommand(that.type, 'false', event.target.value);

            editInstance.dialog.hide();

        }, false);

    }

}

超链接弹窗类

class linkEditor {

  

    constructor(id, btn, type) {

        this.id = 'linkEditor-' + id;

    }

  

    getSelf(id) {

        return document.getElementById(id || this.id);

    }

  

    getTemplate() {

        return `

            <input type="text" id="${this.id}" />

            <button id="${this.id}-okbtn">确定</button>

        `;

    }

  

    show() {

        document.getElementById(this.id).display = 'block';

    }

  

    hide() {

        document.getElementById(this.id).display = 'none';

    }

  

    initEvents(editInstance) {

        const linkEditorBtn = this.getSelf(this.id + '-okbtn');

        const linkEditor = this.getSelf();

        const that = this;

        linkEditorBtn.addEventListener("click", function(event){

            editInstance.editor.document.execCommand('createLink', 'false', linkEditor.value);

            editInstance.dialog.hide();

        }, false);

    }

}

图片插入类

class imageEditor {

  

    constructor(id, btn, type) {

        this.id = 'imageEditor-' + id;

    }

  

    getSelf(id) {

        return document.getElementById(id || this.id);

    }

  

    getTemplate() {

        return `

            <input type="text" id="${this.id}" />

            <button id="${this.id}-okbtn">确定</button>

        `;

    }

  

    show() {

        document.getElementById(this.id).display = 'block';

    }

  

    hide() {

        document.getElementById(this.id).display = 'none';

    }

  

    initEvents(editInstance) {

        const imageEditorBtn = this.getSelf(this.id + '-okbtn');

        const imageEditor = this.getSelf();

        const that = this;

        imageEditorBtn.addEventListener("click", function(event){

            editInstance.editor.document.execCommand('insertImage', 'false', imageEditor.value);

            editInstance.dialog.hide();

        }, false);

    }

}