使用vue开发富文本编辑器原理
日期:2020-01-01
来源:程序思维浏览:2294次
首选了解html5的富文本编辑器的使用方式
<div id="foo" contenteditable style="height:250px; width:350px; background-color:white;font-face:Arial; padding:2; border:inset powderblue; scrollbar-base-color:powderblue; overflow:auto;text-align:left;"></div>
//contenteditable:让div编程文本输入框
<input type="button" value="粗体" unselectable="on" onclick='document.execCommand("Bold"); foo.focus();'>
<input type="button" value="斜体" unselectable="on" onclick='document.execCommand("Italic"); foo.focus();'>
<input type="button" value="下划线" unselectable="on" onclick='document.execCommand("Underline"); foo.focus();'>
<input type=button value="黑体" onclick=document.execCommand('FontName',false,'黑体')>
<input type=button value="9号字" onclick=document.execCommand('FontSize',false,9)>
<input type=button value="红色字" onclick=document.execCommand('ForeColor',false,'#ff0000')>
<br>
<input type=button value="撤消" onclick=document.execCommand('Undo')>
<input type=button value="重做" onclick=document.execCommand('Redo') id=button2 name=button2>
<input type=button value="删除" onclick=document.execCommand('Delete')>
<input type=button value="剪切" onclick=document.execCommand('Cut')>
<input type=button value="拷贝" onclick="document.execCommand('copy');">
<input type=button value="粘贴" onclick="document.execCommand('Paste');">
<br>
<input type=button value="刷新" onclick=document.execCommand('refresh',false,0)>
<input type=button value="停止" onclick=document.execCommand('stop')>
<input type=button value="保存" onclick=document.execCommand('SaveAs')>
<input type=button value="另存为" onclick=document.execCommand('Saveas',false,'c:\test.htm')>
</div>
</center>
<iframe id="HtmlEdit" style="WIDTH: 100%; HEIGHT: 296px" marginWidth=“0” marginHeight=“0”></iframe>
<textarea id="cont"></textarea>
<input type="button" value="粗体" onclick='addblod()'> <input type="button" value="插入图片" onclick='insertImg()'> <input type="button" value="获取内容" onclick='getcon()'>
<script type="text/javascript">
var editor;
editor=document.getElementById("HtmlEdit").contentWindow;
//只需键入以下设定,iframe立刻变成编辑器。
editor.document.designMode = 'On';
editor.document.contentEditable = true;
//但是IE与FireFox有点不同,为了兼容FireFox,所以必须创建一个新的document。
editor.document.open();
editor.document.writeln('<html><body></body></html>');
editor.document.close();
//获取内容
function getcon(){
document.getElementById("cont").value=editor.document.body.innerHTML;
}
//加粗
function addblod(){
editor.focus();
editor.document.execCommand('Bold');
}
//插入图片
function insertImg(){
editor.focus();
insertHTML("<img src='180.jpg' width='20' height='20' />");
}
function insertHTML( sHtml )
{
if(document.selection && document.selection.createRange){
editor.document.selection.createRange().pasteHTML(sHtml) ;//低版本ie插入插入html
}else{
editor.document.execCommand('InsertHtml',false,sHtml);//插入html
}
}
</script>
编写页面遇到的坑
网上大多数富文本编辑器都是iframe,很多成熟的富文本编辑器也是用的iframe。我个人不知道好坏,所以我直接用的div。也许后期会遇到无法解决的坑再换吧。
我不知道是不是我不是用的iframe的问题,但是每当我需要操作文字加粗都失败,原因在于失去选区。
解决:a标签和img标签不会失去选区,改代码。
上个人代码:
<div class="dht-editor-operation">
<template v-for="(item, index) in operationList">
<a :key="index" @click="item.event" :title="item.title">
<img :src="item.iconUrl" :style="item.backgroundImg" alt="" />
</a>
</template>
</div>
我的文字不会加粗
我其实第一个写的功能是颜色选择器,没写很复杂就是用input的color属性
第二功能是文字加粗。但是不会加粗。最后检查得知是因为,我自己有一个全局存在的样式重置代码。我把b标签的默认样式去除了。
到这个时候我都还是用的富文本自己的操作函数
document.execCommand(aCommandName, aShowDefaultUI, aValueArgument)
这里开始就踩坑很严重了。因为样式重置,我考虑到如果别人用我的东西,那么也会这样,那就是说我得避免这个情况。所以我选择了另一条路,自己实现这些功能。当然不可能所有都自己实现,那就太累了,我也不是写产品,给自己用的。
第一次解决方式
var selection = document.getSelection();
//取得选择的文本
var selectionText = selection.toString();
//取得代表选区的范围
var range = selection.getRangeAt(0);
//突出显示选择的文本
var span = document.createElement("span");
span.style.backgroundColor = "yellow";
range.surroundContents(span);
这种方式用的是range.surroundContents,意思是将你选中的文档放在一个新的标签中。
但是问题很严重
缺点:
1、无限的加入标签中,会无限嵌套html元素(自己开发中看f12就懂了)
2、无法跨元素操作
比如这样的,这种选择是不能成功的,报错。因为他无法操作多个dom元素。
这时候已经巨坑了好吧。我原本以为就和别人说的一样,好简单。你要是不考虑那么多操作,不当初编辑器来操作确实简单啊。
第二种解决方式(目前我测试没什么问题)
原理在于先删后插,需要需要各位理解range选区
这里还是会产生无限嵌套问题,我再找找方式,但是解决了跨元素选择问题
代码:
代码:
CursorAcquisition() {
let selection = window.getSelection();
let range = selection.getRangeAt(0);
return {
selection,
range
};
},
//取得代表选区的范围
let range = this.CursorAcquisition().range;
//突出显示选择的文本
//let rangeClone = range.cloneRange();
//获得选中区域dom袁术
let rangeText = range.extractContents();
//创建新的dom并且结合
let span = document.createElement("span");
span.appendChild(rangeText);
span.style.color = "red";
//先移除选中节点
range.deleteContents();
//再插入节点
range.insertNode(span);
这个代码就能实现上述所有产生的问题。
好了第一篇踩坑到此结束,各位请先品尝。后面我还会写下一篇。因为下一个坑我已经踩完了。心累。不写博客我都不知道哪里去发泄。网上的教程即好又缺斤少两,少关键啊。
最后附上,我的颜色选择器,不需要添加html元素。但是需要支持input color
//颜色选择器
colorSelect() {
let input = document.createElement("input");
let that = this;
input.type = "color";
input.click();
input.addEventListener("input", watchColorPicker, false);
function watchColorPicker(event) {
//console.log(event.target.value);
let color = event.target.value;
that.operationList[0].backgroundImg = {
background: color
};
//移除监听
input.removeEventListener("input", watchColorPicker, false);
input = "";
}
}
个人项目代码都在git上面:https://github.com/ht-sauce/dream
总结下富文本遇到的问题:
1、元素跨标签处理
2、如何正确选择到你要的元素
3、跨行设置元素未选中部分换行(默认回车事件导致)
4、多个功能直接交叉使用问题(要有取舍)
5、css新旧混合问题
6、代码块插入问题
7、其实还有更多问题,但是没有开发过的那真的不知其中各种滋味…………
核心代码部分:
这个是我认为的最核心操作部分,这里最核心的就是文档片段的操作。
因为操作原理在于先得到你选中的元素,然后再操作这部分元素,再重新插入进去。(先选后得到元素处理,然后删除重新插入,先删后插)
我的代码没做什么兼容性,从上一次开发到现在满打满算其实是一周的样子。
还有一个比较关键的代码是,基本上自定义的操作都是靠这个api来实现的。
document.execCommand("insertHTML", false, `<br>`);
import { CursorAcquisition } from "./selection";
import { delCss } from "./tool";
//文档片段处理
const domFragmentHandle = () => {
const { range } = CursorAcquisition();
//获取需要操作的元素进行处理
let domst = range.commonAncestorContainer;
domst = domst.nodeType === 1 ? domst : domst.parentNode;
//获取元素中的css属性getSelectionText
let cssText = domst.style.cssText;
let innerhtml = "";
console.log("当前获取的节点", domst.nodeName);
//处理节点名称,若果为指定的元素则返回空
let nodeName = domst.nodeName;
let nodelist = ["div"];
if (nodelist.indexOf(nodeName.toLocaleLowerCase()) !== -1) {
nodeName = "";
}
nodeName = nodeName.toLocaleLowerCase();
//判断处理,如果父节点是最大的div则更换选取方式
if (domst.id === "dht-editor-content") {
let span = document.createElement("span");
let elem = range.cloneContents();
span.appendChild(elem);
innerhtml = span.innerHTML;
} else {
//console.log(domst.childNodes);
//dom元素处理,不要多余标签,将字符串抽离,但是注意保持文档结构,
// 但是该操作会导致多行的文档缩进等无效
let child = domst.childNodes;
for (let i = 0; i < child.length; i++) {
if (child[i].nodeName === "BR") {
innerhtml += `<br>`;
} else {
let node = child[i];
innerhtml += node.nodeValue ? node.nodeValue : node.innerText;
}
}
}
//最终返回需要的元素
return {
innerhtml,
cssText,
nodeName
};
};
//独立选中区域文字
const getSelectionText = () => {
const { range } = CursorAcquisition();
//获取元素中的css属性
let cssText = "";
let span = document.createElement("span");
let elem = range.cloneContents();
span.appendChild(elem);
let innerhtml = span.innerHTML;
//最终返回需要的元素
return {
innerhtml,
cssText
};
};
//最终执行函数
const execOperation = (name, value = null) => {
const { range } = CursorAcquisition();
if (!range.toString()) {
console.log("未选中任何元素");
return false;
}
let bool = document.execCommand(name, false, value);
range.detach();
return bool;
};
//生成html字符串
//传入参数:
/*
* 插入的节点名称,
* style:del: 需要删除的css名称
* css:最后应用的css
* */
const combinationHtml = style => {
const { innerhtml, cssText, nodeName } = domFragmentHandle();
let delcss = style.del || "";
let css = style.css || "";
let node = style.node || "span";
let url = style.url;
//let html = style.html || `<${node} style="${oldCss}; ${css}">${innerhtml}</${node}>`
//之前的css
let oldCss = cssText ? delCss(cssText, delcss) : "";
console.log("html处理得到", nodeName);
//新老节点判断处理,特殊的节点需要使用老节点比如span无法和h2混合
if (nodeName && node !== nodeName && node !== "a") {
node = nodeName;
}
//定义需要插入的html元素
let html = "";
if (node === "a") {
html = `<${node} href="${url}" style="${oldCss}; ${css}">${innerhtml}</${node}>`;
} else {
html = `<${node} style="${oldCss}; ${css}">${innerhtml}</${node}>`;
}
console.log(html);
return html;
};
export { domFragmentHandle, execOperation, getSelectionText, combinationHtml };
<div id="foo" contenteditable style="height:250px; width:350px; background-color:white;font-face:Arial; padding:2; border:inset powderblue; scrollbar-base-color:powderblue; overflow:auto;text-align:left;"></div>
//contenteditable:让div编程文本输入框
<input type="button" value="粗体" unselectable="on" onclick='document.execCommand("Bold"); foo.focus();'>
<input type="button" value="斜体" unselectable="on" onclick='document.execCommand("Italic"); foo.focus();'>
<input type="button" value="下划线" unselectable="on" onclick='document.execCommand("Underline"); foo.focus();'>
<input type=button value="黑体" onclick=document.execCommand('FontName',false,'黑体')>
<input type=button value="9号字" onclick=document.execCommand('FontSize',false,9)>
<input type=button value="红色字" onclick=document.execCommand('ForeColor',false,'#ff0000')>
<br>
<input type=button value="撤消" onclick=document.execCommand('Undo')>
<input type=button value="重做" onclick=document.execCommand('Redo') id=button2 name=button2>
<input type=button value="删除" onclick=document.execCommand('Delete')>
<input type=button value="剪切" onclick=document.execCommand('Cut')>
<input type=button value="拷贝" onclick="document.execCommand('copy');">
<input type=button value="粘贴" onclick="document.execCommand('Paste');">
<br>
<input type=button value="刷新" onclick=document.execCommand('refresh',false,0)>
<input type=button value="停止" onclick=document.execCommand('stop')>
<input type=button value="保存" onclick=document.execCommand('SaveAs')>
<input type=button value="另存为" onclick=document.execCommand('Saveas',false,'c:\test.htm')>
</div>
</center>
<iframe id="HtmlEdit" style="WIDTH: 100%; HEIGHT: 296px" marginWidth=“0” marginHeight=“0”></iframe>
<textarea id="cont"></textarea>
<input type="button" value="粗体" onclick='addblod()'> <input type="button" value="插入图片" onclick='insertImg()'> <input type="button" value="获取内容" onclick='getcon()'>
<script type="text/javascript">
var editor;
editor=document.getElementById("HtmlEdit").contentWindow;
//只需键入以下设定,iframe立刻变成编辑器。
editor.document.designMode = 'On';
editor.document.contentEditable = true;
//但是IE与FireFox有点不同,为了兼容FireFox,所以必须创建一个新的document。
editor.document.open();
editor.document.writeln('<html><body></body></html>');
editor.document.close();
//获取内容
function getcon(){
document.getElementById("cont").value=editor.document.body.innerHTML;
}
//加粗
function addblod(){
editor.focus();
editor.document.execCommand('Bold');
}
//插入图片
function insertImg(){
editor.focus();
insertHTML("<img src='180.jpg' width='20' height='20' />");
}
function insertHTML( sHtml )
{
if(document.selection && document.selection.createRange){
editor.document.selection.createRange().pasteHTML(sHtml) ;//低版本ie插入插入html
}else{
editor.document.execCommand('InsertHtml',false,sHtml);//插入html
}
}
</script>
编写页面遇到的坑
网上大多数富文本编辑器都是iframe,很多成熟的富文本编辑器也是用的iframe。我个人不知道好坏,所以我直接用的div。也许后期会遇到无法解决的坑再换吧。
我不知道是不是我不是用的iframe的问题,但是每当我需要操作文字加粗都失败,原因在于失去选区。
解决:a标签和img标签不会失去选区,改代码。
上个人代码:
<div class="dht-editor-operation">
<template v-for="(item, index) in operationList">
<a :key="index" @click="item.event" :title="item.title">
<img :src="item.iconUrl" :style="item.backgroundImg" alt="" />
</a>
</template>
</div>
我的文字不会加粗
我其实第一个写的功能是颜色选择器,没写很复杂就是用input的color属性
第二功能是文字加粗。但是不会加粗。最后检查得知是因为,我自己有一个全局存在的样式重置代码。我把b标签的默认样式去除了。
到这个时候我都还是用的富文本自己的操作函数
document.execCommand(aCommandName, aShowDefaultUI, aValueArgument)
这里开始就踩坑很严重了。因为样式重置,我考虑到如果别人用我的东西,那么也会这样,那就是说我得避免这个情况。所以我选择了另一条路,自己实现这些功能。当然不可能所有都自己实现,那就太累了,我也不是写产品,给自己用的。
第一次解决方式
var selection = document.getSelection();
//取得选择的文本
var selectionText = selection.toString();
//取得代表选区的范围
var range = selection.getRangeAt(0);
//突出显示选择的文本
var span = document.createElement("span");
span.style.backgroundColor = "yellow";
range.surroundContents(span);
这种方式用的是range.surroundContents,意思是将你选中的文档放在一个新的标签中。
但是问题很严重
缺点:
1、无限的加入标签中,会无限嵌套html元素(自己开发中看f12就懂了)
2、无法跨元素操作
比如这样的,这种选择是不能成功的,报错。因为他无法操作多个dom元素。
这时候已经巨坑了好吧。我原本以为就和别人说的一样,好简单。你要是不考虑那么多操作,不当初编辑器来操作确实简单啊。
第二种解决方式(目前我测试没什么问题)
原理在于先删后插,需要需要各位理解range选区
这里还是会产生无限嵌套问题,我再找找方式,但是解决了跨元素选择问题
代码:
代码:
CursorAcquisition() {
let selection = window.getSelection();
let range = selection.getRangeAt(0);
return {
selection,
range
};
},
//取得代表选区的范围
let range = this.CursorAcquisition().range;
//突出显示选择的文本
//let rangeClone = range.cloneRange();
//获得选中区域dom袁术
let rangeText = range.extractContents();
//创建新的dom并且结合
let span = document.createElement("span");
span.appendChild(rangeText);
span.style.color = "red";
//先移除选中节点
range.deleteContents();
//再插入节点
range.insertNode(span);
这个代码就能实现上述所有产生的问题。
好了第一篇踩坑到此结束,各位请先品尝。后面我还会写下一篇。因为下一个坑我已经踩完了。心累。不写博客我都不知道哪里去发泄。网上的教程即好又缺斤少两,少关键啊。
最后附上,我的颜色选择器,不需要添加html元素。但是需要支持input color
//颜色选择器
colorSelect() {
let input = document.createElement("input");
let that = this;
input.type = "color";
input.click();
input.addEventListener("input", watchColorPicker, false);
function watchColorPicker(event) {
//console.log(event.target.value);
let color = event.target.value;
that.operationList[0].backgroundImg = {
background: color
};
//移除监听
input.removeEventListener("input", watchColorPicker, false);
input = "";
}
}
个人项目代码都在git上面:https://github.com/ht-sauce/dream
总结下富文本遇到的问题:
1、元素跨标签处理
2、如何正确选择到你要的元素
3、跨行设置元素未选中部分换行(默认回车事件导致)
4、多个功能直接交叉使用问题(要有取舍)
5、css新旧混合问题
6、代码块插入问题
7、其实还有更多问题,但是没有开发过的那真的不知其中各种滋味…………
核心代码部分:
这个是我认为的最核心操作部分,这里最核心的就是文档片段的操作。
因为操作原理在于先得到你选中的元素,然后再操作这部分元素,再重新插入进去。(先选后得到元素处理,然后删除重新插入,先删后插)
我的代码没做什么兼容性,从上一次开发到现在满打满算其实是一周的样子。
还有一个比较关键的代码是,基本上自定义的操作都是靠这个api来实现的。
document.execCommand("insertHTML", false, `<br>`);
import { CursorAcquisition } from "./selection";
import { delCss } from "./tool";
//文档片段处理
const domFragmentHandle = () => {
const { range } = CursorAcquisition();
//获取需要操作的元素进行处理
let domst = range.commonAncestorContainer;
domst = domst.nodeType === 1 ? domst : domst.parentNode;
//获取元素中的css属性getSelectionText
let cssText = domst.style.cssText;
let innerhtml = "";
console.log("当前获取的节点", domst.nodeName);
//处理节点名称,若果为指定的元素则返回空
let nodeName = domst.nodeName;
let nodelist = ["div"];
if (nodelist.indexOf(nodeName.toLocaleLowerCase()) !== -1) {
nodeName = "";
}
nodeName = nodeName.toLocaleLowerCase();
//判断处理,如果父节点是最大的div则更换选取方式
if (domst.id === "dht-editor-content") {
let span = document.createElement("span");
let elem = range.cloneContents();
span.appendChild(elem);
innerhtml = span.innerHTML;
} else {
//console.log(domst.childNodes);
//dom元素处理,不要多余标签,将字符串抽离,但是注意保持文档结构,
// 但是该操作会导致多行的文档缩进等无效
let child = domst.childNodes;
for (let i = 0; i < child.length; i++) {
if (child[i].nodeName === "BR") {
innerhtml += `<br>`;
} else {
let node = child[i];
innerhtml += node.nodeValue ? node.nodeValue : node.innerText;
}
}
}
//最终返回需要的元素
return {
innerhtml,
cssText,
nodeName
};
};
//独立选中区域文字
const getSelectionText = () => {
const { range } = CursorAcquisition();
//获取元素中的css属性
let cssText = "";
let span = document.createElement("span");
let elem = range.cloneContents();
span.appendChild(elem);
let innerhtml = span.innerHTML;
//最终返回需要的元素
return {
innerhtml,
cssText
};
};
//最终执行函数
const execOperation = (name, value = null) => {
const { range } = CursorAcquisition();
if (!range.toString()) {
console.log("未选中任何元素");
return false;
}
let bool = document.execCommand(name, false, value);
range.detach();
return bool;
};
//生成html字符串
//传入参数:
/*
* 插入的节点名称,
* style:del: 需要删除的css名称
* css:最后应用的css
* */
const combinationHtml = style => {
const { innerhtml, cssText, nodeName } = domFragmentHandle();
let delcss = style.del || "";
let css = style.css || "";
let node = style.node || "span";
let url = style.url;
//let html = style.html || `<${node} style="${oldCss}; ${css}">${innerhtml}</${node}>`
//之前的css
let oldCss = cssText ? delCss(cssText, delcss) : "";
console.log("html处理得到", nodeName);
//新老节点判断处理,特殊的节点需要使用老节点比如span无法和h2混合
if (nodeName && node !== nodeName && node !== "a") {
node = nodeName;
}
//定义需要插入的html元素
let html = "";
if (node === "a") {
html = `<${node} href="${url}" style="${oldCss}; ${css}">${innerhtml}</${node}>`;
} else {
html = `<${node} style="${oldCss}; ${css}">${innerhtml}</${node}>`;
}
console.log(html);
return html;
};
export { domFragmentHandle, execOperation, getSelectionText, combinationHtml };
精品好课