使用vue开发富文本编辑器原理

来源:程序思维浏览: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()'>&nbsp;&nbsp;<input type="button" value="插入图片" onclick='insertImg()'>&nbsp;&nbsp;<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 };
精品好课
HTML5视频播放器video开发教程
适用人群1、有html基础2、有css基础3、有javascript基础课程概述手把手教你如何开发属于自己的HTML5视频播放器,利用mp4转成m3u8格式的视频,并在移动端和PC端进行播放支持m3u8直播格式,兼容...
最新完整React+VUE视频教程从入门到精,企业级实战项目
React和VUE是目前最火的前端框架,就业薪资很高,本课程教您如何快速学会React和VUE并应用到实战,教你如何解决内存泄漏,常用库的使用,自己封装组件,正式上线白屏问题,性能优化等。对正在工作当中或打算学习Re...
最新完整React视频教程从入门到精通纯干货纯实战
React是目前最火的前端框架,就业薪资很高,本课程教您如何快速学会React并应用到实战,教你如何解决内存泄漏,常用UI库的使用,自己封装组件,正式上线白屏问题,性能优化等。对正在工作当中或打算学习React高薪就...
React实战视频教程仿京东移动端电商
React是前端最火的框架之一,就业薪资很高,本课程教您如何快速学会React并应用到实战,对正在工作当中或打算学习React高薪就业的你来说,那么这门课程便是你手中的葵花宝典。
jQuery视频教程从入门到精通
jquery视频教程从入门到精通,课程主要包含:jquery选择器、jquery事件、jquery文档操作、动画、Ajax、jquery插件的制作、jquery下拉无限加载插件的制作等等......
VUE2+VUE3视频教程从入门到精通(全网最全的Vue课程)
VUE是目前最火的前端框架之一,就业薪资很高,本课程教您如何快速学会VUE+ES6并应用到实战,教你如何解决内存泄漏,常用UI库的使用,自己封装组件,正式上线白屏问题,性能优化等。对正在工作当中或打算学习VUE高薪就...
Vue2+Vue3+ES6+TS+Uni-app开发微信小程序从入门到实战视频教程
2021年最新Vue2+Vue3+ES6+TypeScript和uni-app开发微信小程序从入门到实战视频教程,本课程教你如何快速学会VUE和uni-app并应用到实战,教你如何解决内存泄漏,常用UI库的使用,自己...
HTML5基础入门视频教程易学必会
HTML5基础入门视频教程,教学思路清晰,简单易学必会。适合人群:创业者,只要会打字,对互联网编程感兴趣都可以学。课程概述:该课程主要讲解HTML(学习HTML5的必备基础语言)、CSS3、Javascript(学习...
收藏
扫一扫关注我们