H5可编辑属性contenteditable实现富文本编辑器

使用HTML5新属性contenteditable实现可插入链接、表情包、其他变量的编辑器,因为在我使用这个功能是在19年项目中需求中有涉及,最近被问到一些关于该功能的问题,就做一下总结.

前序说明:

1、技术栈: vue@^2.7.14, element-ui@^2.13.1, emoji@^0.3.2,  js, html, css
2、div可编辑属性,change事件失效,可通过监听input事件来时时得到输入内容的变化
3、开发此功能是为了实现用微信公众号向用户推送客服消息时,创建文本消息内容开发的,微信开放平台对于文本消息("msgtype":"text",)内容的格式有限制:文本中只支持a标签
去微信开放平台

微信开放平台提供的接口:

先看页面效果

可插入emoji表情,可插入a链接,可插入小程序链接,还可以插入一些自定义的变量

公众号发送到用户看到的效果

前期准备------首先简单介绍一下contenteditable
contenteditable 属性是 HTML5 中的新属性。规定是否可编辑元素的内容。

(1)属性值
true 规定可以编辑元素内容。
false 规定无法编辑元素内容。

<div contentEditerable = true>此处内容可编辑</div>

(2)contenteditable 与textarea的区别
   1. textarea支持多行文本输入,满足了我们编辑的很大需求。然而,textarea不能像div一样高度自适应,高度保持不变,内容大于高度时就会出现滚动条;
   2. textarea只支持文本输入,随着现在越来越关注用户体验,需求也越来越多,很多时候我们需要在编辑区域插入图片,链接,视频;
   3. 传统textarea文本域不能解析标签,例如:extarea.value+=""//输入框内仍然是,不能解析标签,自然就不能使用textarea作为文本载体了,我们可是使用conteneditable属性,它可以让你的div也具备输入功能,div可以输入内容了,并且插入的标签也可以解析;如果只是想在段落末尾加上表情,那你大可以这样去做:div. innerHTML+=""

(3)getSelection(获取selection对象)
Selection对象所对应的是用户所选择的ranges(区域),俗称拖蓝。默认情况下,该函数只针对一个区域

var  sel =window.getSelection();
var  range = sel.getRangeAt(0)  //选择第一选区
range.collapse(false);//对于IE来说,参数不可省略
range.insertNode(node);//节点插入到该选区

这样去写的话会存在一些问题;当你框选内容的时候,不会替换内容,而是在所选内容之后插入,这是因为range.collapse()方法

range.collapse();//(false默认)到选区末端, true开始位置, //当你框选内容的时候,执行该方法,可以让光标移动到选区结束位置,然后插入内容

所以,当框选的时候,正常做法应该是删除框选内容,然后插入新节点
range.deleteContents();//清除内容

完整代码:
functioninsertImg(src){
      if(window.getSelection) {
            var sel =window.getSelection();
            var range = sel.getRangeAt(0);
            var img =newImage(); 
            range.deleteContents()
            img.src=src;
            range.insertNode(img);
            range.collapse(false);//对于IE来说,参数不可省略
       }
 }

(4)contenteditable兼容性

(5)contenteditable其他知识点
让contenteditable元素只能输入纯文本

css控制法
一个div元素,要让其可编辑,contenteditable属性是最常用方法,CSS中也有属性可以让普通元素可读写。
user-modify (属性介绍https://blog.csdn.net/weixin_30362233/article/details/98374335)
支持属性值如下:

user-modify:read-only;
user-modify:read-write;
user-modify: write-only;//可以输入富文本
user-modify:read-write-plaintext-only;//只能输入纯文本

read-write和read-write-plaintext-only会让元素表现得像个文本域一样,可以focus以及输入内容

(2)contenteditable控制法

contenteditable="plaintext-only"    //  "plaintext-only"可以让编辑区域只能键入纯文本

*注意:目前仅仅是Chrome浏览器支持比较好的


一、编辑器实现

1、输入功能
div标签可编辑
这一步比较简单,只需要给div标签添加contenteditable为true即可;

<div contenteditable="true" style="height:100px; border: 1px solid red; padding:2px;" id="editor" ref="editor">

</div>

通过监听input事件,时时关注内容的变化并获取输入内容

//let editor = document.getElementById('editor')
 //editor.addEventListener('input', (item) => { console.log(item) })

this.$refs.editor.addEventListener('input', this.changeContentValue);

自动获取焦点

// let editor = document.getElementById('editor')
 // editor.focus();

this.$refs.editor.focus();

光标位置定位,往光标处插入html片段

// 往光标位置插入HTML片段
function insertHtmlAtCaret(html) {
     if (window.getSelection) {
         // IE9 and non-IE
          if (this.sel.getRangeAt && this.sel.rangeCount) {
                   var el = document.createElement('div');
                    el.innerHTML = html;
                    var frag = document.createDocumentFragment();
                    var node;
                    var lastNode;
                    while ((node = el.firstChild)) {
                         lastNode = frag.appendChild(node);
                     }
                   this.range.insertNode(frag);
                  if (lastNode) {
                         this.range = this.range.cloneRange();
                          this.range.setStartAfter(lastNode);
                          this.range.collapse(true);
                          this.sel.removeAllRanges();
                           this.sel.addRange(this.range);
                      }
                 }
            }
            else if (document.selection && document.selection.type !== 'Control') {
                     // IE < 9 document.selection.createRange().pasteHTML(html);
              }
     },

2、插入a链接功能
点击插入链接按钮可出现弹窗插入或者修改内容

在点击插入链接按钮(也就是输入框失去焦点)的时候获取光标所在的位置
   this.sel = window.getSelection();
   this.range = this.sel.getRangeAt(0);
   this.taget = this.sel.focusNode.parentElement;
   const { sel, taget } = this;

选中一部分内容,或者点解已插入链接的内容
第一次添加链接或者多次修改链接内容

   this.selectContents = sel.toString(); // 当选中未添加链接的内容时,选中内容复制给链接的文字字段

显示弹窗,对弹窗的文本与链接进行修改
const { selectContents, selectUrl} = this;
this.$set(this.textForm, 'text', selectContents);
this.$set(this.textForm, 'url', selectUrl);

完成后点击确定,以新内容替换旧内容
const { text, url } = this.textForm;
 if (text && url) {
 this.range && this.range.deleteContents(); // 删除输入框原有的文本内容
 const { selectContents, selectUrl, taget } = this;
 if (selectContents && selectUrl && taget) {
     Array.from(this.$refs.editor.childNodes).forEach((item) => {
         if (item === taget) {
                this.$refs.editor.removeChild(taget); // 删除输入框原有的文本链接内容 }
          else if (taget.parentNode === item) {
                 item.removeChild(taget); // 当村子a链接内有插入了一次a标签的情况处理
               }
          });
     }

插入到输入框
 this.insertHtmlAtCaret(`<a href='${url}' style="color:#5392ff">${text}</a>`); } 
 this.textForm = { url: '', text: '' }; // 重置

效果图

3、插入小程序链接同上

4、插入表情包功能
封装emoji 组件

 
引入emoji组件

import Emoji from './emoji';
const emoji = require('emoji');
components: {
 Emoji
 }
html部分
<el-popover
     ref="popover-click"
     placement="bottom-start"
      width="390"
      trigger="click"
        @show="mountedEmoji = true"
  >
         <Emoji
            @emoji = "selectEmoji">
        </Emoji>
 </el-popover>

 插入表情
 function selectEmoji(emoji) {
     this.insertHtmlAtCaret(emoji);
 },

5、输入字数统计功能
     div的可编辑属性,获取到的内容格式如下,如果统计输入字数需要对其进行处理

从获取到的输入内容可得出的结论是
  (1) shift+回车换行会在当前操作的这一行后生成<br/>标签,用来与下一行内容分开
  (2) 直接回车换行会生成<div><br/></div>形式, 输入内容后,输入的内容替换div标签中的br
  (3)当使用了直接回车换行,再使用shift+回车换行,则shift+回车换行这行内容会被直接回车换行生成的div包裹
  (4) 光标处于0位置的时候禁止换行

针对以上需求处理方法是,对div中输入的内容进行过滤

function getDomValue(elem) {
    var res = '';
    let arr = Array.from(elem.childNodes);
     arr.forEach((child) => {
         if (child.nodeName === '#text') {
               res += child.nodeValue;
          } else if (child.nodeName === 'BR') {
                res += '\n';
           } else if (child.nodeName === 'P') {
              res += '\n' + getDomValue(child);
           } else if (child.nodeName === 'SPAN') {
           res += getDomValue(child);
           } else if (child.nodeName === 'BUTTON') {
          res += getDomValue(child);
          } else if (child.nodeName === 'IMG') {
             res += child.alt;
           } else if (child.nodeName === 'DIV') {
                 const s = Array.from(child.childNodes);
              if (s.length === 1 && s[0].nodeName === 'BR' || child.previousSibling && child.previousSibling.nodeName === 'BR') {
 // 处理shift+回车与直接回车混用导致多处来换行的情况
    res += getDomValue(child); }
           else {
                res += '\n' + getDomValue(child); 
            }
           )else if (child.nodeName === 'A') {
                if (child.href !== null) {
                     const innerHTML = child.innerHTML.replace(/<br>/g, '')
                                        .replace(/<span (.*?)>/gi, '').replace(/<\/span>/gi, '');
                     res += `<a href='${child.href}'>${innerHTML}</a>`;
                 }
        }
}

统计字数
function getDomValuelength(elem) {
     var reg = /<a[^>]+?href=["']?([^"']+)["']?[^>]*>([^<]+)<\/a>/gi;
     var data = elem.toLowerCase().replace(reg, function ($1, $2, $3) {
                         return $3;
               });
      return data.length;
}

6、我的源码git地址:https://github.com/wangAlisa/div-follow-input

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容