长期以来,做为用户我是富文本编辑器的使用者,做为前端开发,我也只是富文本插件的使用者,对内部实现细节不甚了解,使用上也只停留在调用插件提供的API,实现一些业务逻辑。最近的项目,须要开发一个简易富文本编辑器,也算是让我有机会对其一窥究竟。javascript
咱们知道form表单中的input、textarea之类标签是支持内容可编辑的,但并不支持富文本,若是在这些标签里粘贴带格式的内容,会被去格式,只保留文本内容。若是想设置可编辑富文本,有两种方式:html
designMode
属性值为“on”,这样整个文档就变得能够编辑。<iframe
name="richtext" src="blank.html"></iframe>
window.addEventListener("load",function (){
frames("richtext").document.designMode = "on"
});
复制代码
须要在嵌入页面加载以后,动态设置iframe文档的designMode
属性。前端
contenteditable
属性该属性最先是由IE实现,且能够做用于页面中的任何标签,只须要在文档里给标签设置以上属性便可,无需嵌入iframe、设置js属性,因此这种方式也是目前富文本编辑器插件中更多采用的方式;java
<div class="editbox" id="richtext" contenteditable>
<p></p>
<p contenteditable="false"></p>
</>
复制代码
这样,此div元素中包含的内容就能够编辑了,固然也能够设置子元素(如第二个P元素)为不可编辑。经过js设置元素的该属性,也能够改变编辑模式:web
var elm = document.getElementById('richtext');
elm.contentEditable = 'true';
复制代码
contenteditable
属性有三个可能的值:'true'表示打开编辑模式,'false'表示关闭,'inherit'表示从父元素继承此属性值。contenteditable
属性兼容性较好,在主流浏览器包括IE以及目前大部分的移动端浏览器上,都获得支持。canvas
常见的富文本编辑器插件,如wangEditor、百度的UEditor,都有各类丰富的菜单区域来设置编辑内容及格式,如常规的设置标题、文字加粗、超连接等,更胜者插入图片、视频及自定义的内容结构等,而实现这些功能的API就是document.execCommand()
,这个方法是与富文本编辑器进行交互的主要方式。segmentfault
语法:设计模式
bool = document.execCommand(aCommandName, aShowDefaultUI, aValueArgument)
复制代码
全部支持的commands,可查阅MDN;其中,与剪贴板相关的命令(copy、cut、paste)各浏览器实现差别较大,使用时需关注浏览器差别。api
经常使用的命令举例:跨域
// 加粗
document.execCommand('bold', false, null);
// 超连接
document.execCommand('createlink', false, 'https://www.kaola.com');
// 格式化为h1标题
document.execCommand('formatblock', false, '<h1>');
复制代码
【注意】虽然全部浏览器都支持以上命令,但这些命令生成的html结构仍有差异。如bold命令,IE和Opera会使用<strong>
标签包裹文本,而Safari和Chrome则使用<b>
标签,firefox使用<span>
。
var canBold = document.queryCommandEnabled("bold");
复制代码
var isBold = document.queryCommandState("bold");
复制代码
可使用这个方法,来设置编辑器中加粗、斜体等按钮的状态。
Seletion对象是指用户选中的文本范围或鼠标的当前位置,经过window.getSelection()
来获取该对象。
Seletion对象的属性以下:
Selection对象的方法参阅MDN。这些方法在富文本编辑器插件里都是颇有用的方法,好比控制光标的方法collapse()、collapseToEnd()、collapseToStart()
,能够设置插入内容以后光标的位置; 获取选区包含的文本的方法toString()
;getRangeAt(index)
方法返回索引对应的选区中的DOM范围,即range对象
来看一个例子:
// 获取选区内容的位置
function getSelPos () {
let sel = window.getSelection()
let rg = sel.getRangeAt(0)
let elmRect = rg.getClientRects()[0]
let editorRect = $('.j-editor')[0].getBoundingClientRect() // 编辑器容器
let pos = {}
if (elmRect) {
// 选区内容居中位置距容器的左距离
pos.x = elmRect.left - editorRect.left + elmRect.width / 2
pos.y = elmRect.top - editorRect.top
}
return pos
}
复制代码
上述方法,能够获取当前选区相对于编辑器容器的位置,能够用来设置在选区附近出现的工具条等。 想实时监测选区的变化,能够监听onselectionchange
事件
// 高频事件,作好节流
document.onselectionchange = _.debounce(this.onSelect, 100)
复制代码
若是往富文本编辑器里粘贴内容,是会把内容的样式也粘贴进来的,浏览器自动会把应用到某个标签的样式内联到此标签的style属性。但更多的时候咱们只是须要保留里面的部分格式,须要针对剪贴板中的内容进行过滤、格式化以及特定内容保留等。
editorElem.on('paste', event => {
event.preventDefault();
let clipboardData = event.clipboardData || event.originalEvent && event.originalEvent.clipboardData || {};
let text = clipboardData.getData('text/plain');
let html = clipboardData.getData('text/html');
})
复制代码
经过侦听paste事件,能获取到事件对象上的clipboardData对象,获取粘贴的内容,能够经过getData方法获取剪切版上的纯文本或html结构。 有了html结构,就能够转成dom对象,针对处理了。默认状况下,剪贴板中的
下面重点说下对剪贴板中图片的处理。若是剪贴板中的网页元素包含图片,即img标签,若是直接粘贴到编辑器,该图片的连接地址是原网页所在的图片地址,这里就要考虑到,若是外链别人网站的图片,就有可能有朝一日这个图片不可用,因此仍是要放到自家的服务器上才放心。这里就涉及到,已知一张图片的可访问的连接地址,如何把该图片上传到本身的服务器上呢?
不考虑兼容性,给出一种可行的方案:经过canvas画布获取图片的数据,并将数据转为blob对象并进行上传。步骤以下:
// 根据图片的url,上传图片
export function uploadImgWithUrl(imgUrl, editor) {
/**
* 数据转blob对象
*/
function dataToBlob(data) {
var bytes = void 0;
bytes = data.split(",")[0].indexOf("base64") >= 0 ? window.atob(data.split(",")[1]) : unescape(data.split(",")[1]);
var paramType = data.split(",")[0].split(":")[1].split(";")[0];
var uArr = new Uint8Array(bytes.length);
for (let i=0; i < bytes.length; i++) {
uArr[i] = bytes.charCodeAt(i);
}
return new Blob([uArr], {
type: paramType,
name: 'blob.png'
});
}
var options = this;
return new Promise(function(resolve, reject) {
var img = new Image;
img.setAttribute("crossOrigin", "anonymous");
img.onload = function() {
var canvas = document.createElement("canvas");
canvas.width = img.width;
canvas.height = img.height;
canvas.getContext("2d").drawImage(img, 0, 0);
var data = canvas.toDataURL("image/png");
var blob = dataToBlob(data);
blob.name = 'blob.jpg'
// 调用编辑器的上传图片接口 or 也可自行实现一个图片上传方法
editor.uploadImg.uploadImg([blob], function(result){
if (result && result.body) {
var link = result.body.imageUrlList || [];
var item = {
resourceType: 'image',
imageUrl: link[0]
}
resolve(item);
}
})
};
img.onerror = function() {
reject();
Message.error('图片不容许跨域访问,请手动下载后添加')
};
imgUrl = -1 !== imgUrl.indexOf("?") ? imgUrl + "&time=" + (new Date).getTime() : imgUrl + "?time=" + (new Date).getTime();
img.src = imgUrl;
});
}
复制代码
以上是将在线的图片上传到服务器的一种解决方法,也在项目中进行了实践。对于剪贴板内存中的图片内容,能够经过getAsFile()
方法来获取进而上传:
// 处理内存中的图片
if (clipboardData.items[0]) {
let item = clipboardData.items[0]
let type = item.type;
let regResult = type.match(/image\/(.+)/)
if (regResult) {
let blob = item.getAsFile();
// 调用编辑器的通用上传接口
editor.uploadImg.uploadImg([blob])
}
}
复制代码
以上,只算上对富文本编辑器的基本知识点进行了初步的梳理,若是想本身造轮子,撸一个编辑器出来,须要解决的问题还有不少,能够看下知乎上的讨论为何都说富文本编辑器是天坑?,里面提到实现一个使人满意的编辑器须要各类填坑,以及良好的设计模式,路漫漫其修远兮……
by lzf