contenteditable实现编辑器,光标、输入法处理,emoji的显示和转换存储

需要开发一个移动端的富文本编辑器,但是不想用uedit等富文本编辑器,那就只能自己支持了。

1、contenteditable="true",对组件设置contenteditable="false",这俩是前提

2、placeholder,不要写在content里面,用样式empty来设置,否则插入组件之后,删除会自动清空。应该在外层使用div,用绝对定位,移动到你想要的位置。

 .content:empty::before{
     content: attr(placeholder);
     font-size: 14px;
     color: #CCC;
     line-height: 21px;
     padding-top: 10px;
 }
请输入不少于150字正文,支持图文商品混排哦!

contenteditable实现编辑器,光标、输入法处理,emoji的显示和转换存储_第1张图片

3、监听content的focus方法,当光标聚焦的时候,给元素插入default元素。这里开始琢磨了我一段时间,因为默认的插入文本都是在content里面,如输入aaaa:

aaaa
,然后回车
aaaa
,显然,如果是这种样式,不符合规定,且不好操作,之前一直想着替换,单问题很多:光标,回车不自动出现回车效果,而是里面嵌套。如下设置之后,则完全符合预期。

const defaultHtml = '';

var dom = document.getElementById("content");

if(dom.innerHTML==""){
   dom.innerHTML=defaultHtml;
}

回车之后:

4、监听input方法,如果用户,删除了,内容没数据,需要显示placeholder,查找p标签的内容,记录文本字数,还需要判断是否有插入组件,这里注意,如果用户点击删除,没内容之后,继续删除,那么可能会默认的defaultHtml都没有,且丢失焦点,这样子代理的问题是插入表情的时候就不符合预期了,表情应该是在p里面。

let item = self.dom.getElementsByClassName("feedback_mix_text");
    let num = 0;
    for(let i = 0,len=item.length;i

4、最核心的,需要记录最后的光标位置。这里还判断了光标位置,如果当前光标不再content里面的text标签里面,则不需要记录,我这边输入内容都会在

里面。

document.addEventListener('selectionchange',function(){
             getCursor(self);
});
/**
 * 获取光标位置
 */
function getCursor(self){
    var sel = getSelection();
    if(!sel){
        return;
    }
    var node = sel.anchorNode;
    var isIn = false;
    while(node&&node.nodeType!=node.DOCUMENT_NODE){
        var cls = node.classList;
        if(cls&&cls.contains("feedback_mix_text")){
            isIn = true;
            break;
        }
        node=node.parentNode
    }
    if(!isIn) return;
    console.log("getCursor");
    self.select = sel;
    self.lastRange = sel.getRangeAt(0);
}

5、插入元素。分为插入表情,粘贴,插入其他正常商品,图片等。

var sel = this.select;
            var range = this.lastRange;
            if(!sel||!range) return;
            var el;
            if(type=="emoji"){
                el = document.createElement("img");
                el.className="quan_icon_emoji";
                el.src=opt.url;
            }else if(type=="paste"){
                el = document.createElement("p");
                el.className="feedback_mix_text citem";
                el.innerText = opt.tpl;
            }else{
                el = document.createElement('div');
                el.innerHTML = opt.tpl;
                el.className="citem";
            }
            range.insertNode(el);
            
            afterInserDom(this,el,type);

6、处理元素,插入的节点,会在p标签里面,但是实际上应该和P标签并列。所以需要处理。如果是emoji的话,不应该换行。这里插入了元素

function afterInserDom(self,lastNode,type){
    if(type=="emoji"){
        domUtil.deleteBr(lastNode);
    }else{
        domUtil.breakParent(lastNode,lastNode.parentNode);
    }
    self.component_num++;
    if(self.content_num==0){
        textChange(self);
    }
    
}
function breakParent(node, parent) {
    var tmpNode,
      parentClone = node,
      clone = node,
      leftNodes,
      rightNodes;
    do {
      parentClone = parentClone.parentNode;
      //保护,防止出现插入内容不是在

里面,那么则不需要breank,否则会跑到content之外 if(parentClone.id=="content"){ return; } leftNodes = parentClone.cloneNode(false); rightNodes = leftNodes.cloneNode(false); while ((tmpNode = clone.previousSibling)) { leftNodes.insertBefore(tmpNode, leftNodes.firstChild); } while ((tmpNode = clone.nextSibling)) { rightNodes.appendChild(tmpNode); } //如果右边没有数据了,则需要插入br,否则会获取不了焦点。 if(rightNodes&&rightNodes.nodeName=="P"&&rightNodes.innerHTML==""){ rightNodes.appendChild(document.createElement("br")); } //删除左边的空p标签 if(leftNodes&&leftNodes.nodeName=="P"&&leftNodes.innerHTML==""){ leftNodes=""; } clone = parentClone; } while (parent !== parentClone); tmpNode = parent.parentNode; leftNodes&&tmpNode.insertBefore(leftNodes, parent); tmpNode.insertBefore(rightNodes, parent); tmpNode.insertBefore(node, rightNodes); remove(parent); return node; } function remove(node) { var parent = node.parentNode; if (parent) { parent.removeChild(node); } return node; }
function deleteBr(node){
  var next = node.nextSibling;
  if(next&&next.nodeName=="BR"&&next.parentNode.nodeName=="P"){
    remove(next);
  }
}

7、移动光标。插入元素之后,有把节点调整了位置,则已经失去光标了,需要把光标移动插入元素之后。

function moveRange(self,el,range){
    var sel = self.select;
    if(!sel){
        console.log(sel);
        return;
    }
    range = (range||self.lastRange).cloneRange();
    if(el){
        if(!el.nextSibling&&el.nodeName=="P"){
            range.setStart(el,0);
        }else if(el.nextSibling){
            range.setStart(el.nextSibling,0);
        }else{
            range.setStartAfter(el);
        }
    }
    range.collapse(true);
    sel.removeAllRanges();
    sel.addRange(range);
}

8、富文本可以粘贴,所以需要处理用户粘贴的情况。这次暂时不处理图片,文章的粘贴,如有需要,可以data.items[0].getAsFile()来获取图片。

wrap.addEventListener("paste",function (event) {
        var data = event.clipboardData;
        if(!data||(data.files&&data.files.length>0)){//not support or copy file
            event.returnValue = false;
            return false;
        }
        //如果当前已经有update,且时间是100ms以内,则认为先textchange,再paste,这不是标准的paste,需要拦截。
        var update = store.state.flag.update;
        if(update&&Date.now()-update<100){
            return;
        }
        handlePaster();
    });

9、获取要粘贴的内容。之前还傻傻的想直接获取文件内容,event.clipboardData.items[0].getAsString()  没有用,没有用。

/**
 * 处理复制内容
 */
function handlePaster() {
    var sel = getSelection();
    var range = sel.getRangeAt(0).cloneRange();
    var div = document.createElement("div");
    div.id = "gwq_paste";
    div.setAttribute("contenteditable","true");
    div.style.cssText ="position:absolute;width:1px;height:1px;overflow:hidden;left:-1000px;white-space:nowrap;top:"+window.pageYOffset+"px";
    div.innerHTML = "
"; document.body.appendChild(div); range.setStart(div,0); range.collapse(true); sel.removeAllRanges(); sel.addRange(range); setTimeout(function () { var pastedom = document.querySelector("#gwq_paste"); var text = pastedom.innerText; pastedom.remove(); JD.events.trigger("afterpaste",text); },0); }

10、禁止拖动和移动,

//prevent drag
    wrap.addEventListener('dragover', function(event){
        event.preventDefault();
        return false;
    });
    //prevent drop
    wrap.addEventListener('drop', function(event){
        event.preventDefault();
        return false;
    });

11、将输入法输入的表情转换成unicode

/**
 * emoji转换成unicode存储,\ud83c\udf4f
 * 然后innerHTMl="\ud83c\udf4f"即可显示表情
 * @param {*} emoji 
 */
function emoji2Unicode(emoji) {
  var backStr = '';
  if (emoji && emoji.length > 0) {
      for (var char of emoji) {
          var index = char.codePointAt(0);
          if (index > 65535) {
              var h =
                  '\\u' +
                  (Math.floor((index - 0x10000) / 0x400) + 0xd800).toString(
                      16
                  );
              var c =
                  '\\u' + ((index - 0x10000) % 0x400 + 0xdc00).toString(16);
              backStr = backStr + h + c;
          } else {
              backStr = backStr + char;
          }
      }
  }
  return backStr;
}
/**
 * //unicode 转换为实体字符以供后台存储
 * unicode2Enti("\ud83c\udf4f")  ---》"🍏"
 * 然后innerHTMl="🍏"即可显示表情
 * @param {*} str 
 */
function unicode2Enti(str) {
  var patt = /[\ud800-\udbff][\udc00-\udfff]/g;
  str = str.replace(patt, function(char) {
      var H, L, code;
      if (char.length === 2) {
          //辅助平面字符(我们需要做处理的一类)
          H = char.charCodeAt(0); // 取出高位
          L = char.charCodeAt(1); // 取出低位
          code = (H - 0xd800) * 0x400 + 0x10000 + L - 0xdc00; // 转换算法
          return '&#' + code + ';';
      } else {
          return char;
      }
  });
  return str;
}
function isEmoji(substring) {  
  for ( var i = 0; i < substring.length; i++) {  
      var hs = substring.charCodeAt(i);  
      if (0xd800 <= hs && hs <= 0xdbff) {  
          if (substring.length > 1) {  
              var ls = substring.charCodeAt(i + 1);  
              var uc = ((hs - 0xd800) * 0x400) + (ls - 0xdc00) + 0x10000;  
              if (0x1d000 <= uc && uc <= 0x1f77f) {  
                  return true;  
              }  
          }  
      } else if (substring.length > 1) {  
          var ls = substring.charCodeAt(i + 1);  
          if (ls == 0x20e3) {  
              return true;  
          }  
      } else {  
          if (0x2100 <= hs && hs <= 0x27ff) {  
              return true;  
          } else if (0x2B05 <= hs && hs <= 0x2b07) {  
              return true;  
          } else if (0x2934 <= hs && hs <= 0x2935) {  
              return true;  
          } else if (0x3297 <= hs && hs <= 0x3299) {  
              return true;  
          } else if (hs == 0xa9 || hs == 0xae || hs == 0x303d || hs == 0x3030  
                  || hs == 0x2b55 || hs == 0x2b1c || hs == 0x2b1b  
                  || hs == 0x2b50) {  
              return true;  
          }  
      }  
  }  
}  

 

你可能感兴趣的:(js)