经过前几节的层层递进,我们了解了一个利用原生的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);
}
}