web前端技术分享

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

2023-07-24  本文已影响0人  廊桥梦醉

使用div标签的可编辑属性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链接,可插入小程序链接,还可以插入一些自定义的变量

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

一、富文本实现

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

上一篇下一篇

猜你喜欢

热点阅读