1. ホーム
  2. js

contenteditableは、エディタ、カーソル、入力メソッド処理、絵文字表示、変換ストレージを実装しています。

2022-02-27 04:02:33

モバイル用のリッチテキストエディタを開発したいのですが、ueditのようなリッチテキストエディタは使いたくないので、自分で対応する必要があります。

1. contenteditable="true", set contenteditable="false" for the component, these two are prerequisites.

<div id="content" contenteditable="true" class="content"></div>
<div class="feedback_mix_img" contenteditable="false" data-type="image">
    <img src="blob:http://wqs.jd.com/69573dec-eb6c-417c-b6f9-3124d071bfa8">
    <div class="operator_item">
        <div class="circle">
            <div class="up" data-action="up"></div>
        </div>
        <div class="circle">
            <div class="down" data-action="down"></div>
        </div>
        <div class="circle change changeImage">changeImage</div>
    </div>
    <div class="del_item" data-action="del">
        <div></div>
    </div>
</div>

 .content:empty::before{
     content: attr(placeholder);
     font-size: 14px;
     color: #CCC;
     line-height: 21px;
     padding-top: 10px;
 }
<div class="placeholder"> Please enter no less than 150 words of body text, support graphics and merchandise mix oh! </div>

2、プレースホルダーは、コンテンツの内部に書き込まないでください、設定するには、空のスタイルを使用して、それ以外の場合は、コンポーネントを挿入した後、削除が自動的にクリアされます。あなたが望む位置に移動するには、絶対位置決めで、外側のレイヤーにdivを使用する必要があります。

const defaultHtml = '<p class="feedback_mix_text citem"><br/></p>';

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

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

let item = self.dom.getElementsByClassName("feedback_mix_text");
    let num = 0;
    for(let i = 0,len=item.length;i<len;i++){
        num+=item[i].innerText.length;
    }
    self.content_num = num;
    let style = self.placeholder.style;
    if(self.dom.innerHTML==""&&self.component_num==0){
        style.display="block";
    }else{
        style.display="none";
    }
if(!item||item.length==0){
        self.dom.innerHTML=defaultHtml;
        setTimeout(function(){
            moveRange(self,self.dom.querySelector("p"));
        },0);
    }

<イグ

3. contentのfocusメソッドを聞いて、カーソルがフォーカスされた時にその要素にデフォルトの要素を挿入する。ここで、しばらく考え始めたのですが、デフォルトの挿入テキストは、input aaaaのように、コンテンツの中にあるためです。 <div id="content">aaaa</div> それから、 <div id="content"> aaaa<div></div></div> 明らかに、このスタイルなら、規制に準拠していないと悪い操作、常に交換を考える前に、単一の問題がたくさん:カーソル、入力が自動的に効果を返す馬車表示されない、しかし内部にネストされています。次の設定の後、それは正確に期待通りです。

document.addEventListener('selectionchange',function(){
             getCursor(self);
});
/**
 * Get the position of the cursor
 */
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);
}

キャリッジリターンの後。

4は、入力メソッドに耳を傾け、ユーザーは、削除された場合は、データのコンテンツは、プレースホルダを表示する必要がある、pタグの内容を見つけるには、テキストの単語の数を記録するだけでなく、そこに挿入されているかどうかを判断する必要があるコンポーネント、ここでユーザーは、削除するコンテンツ後にクリックした場合は、削除を継続、その後デフォルトのデフォルトHtmlされていない可能性があり、このように、フォーカスを失ったことに注意 プロキシの問題は、式の挿入が期待どおりに、式のP内部する必要がありますがされていませんです。

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);


4、コア、最後のカーソル位置を記録する必要があります。ここでもカーソル位置を決定し、現在のカーソルは、もはやコンテンツ内のテキストタグの内部にある場合、それは記録する必要はありません、入力コンテンツの私の側は、<pクラス= "feedback_mix_text citem"></p> 内にされます。

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;
      //protect against insertions that are not in <p></p> inside, then no breank, otherwise it will run outside the 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);
      }
      //If there is no more data on the right, then you need to insert br, otherwise it will not get the focus.
      if(rightNodes&&rightNodes.nodeName=="P"&&rightNodes.innerHTML==""){
        rightNodes.appendChild(document.createElement("br"));
      }
      //delete the empty p tag on the left
      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);
  }
}

5. Insert要素です。顔文字の挿入、貼り付け、その他通常のアイテムの挿入、画像などに分けられる。

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);
}

6. 挿入されたノードはpタグの中に入りますが、実際にはpタグと並んでいるはずなので、それを処理します。だから、それを処理する必要があります。絵文字であれば、改行されるべきではないでしょう。ここでは、要素が挿入されています。

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;
        }
        //If there is already an update, and the time is within 100ms, it is considered that textchange, then paste, which is not the standard paste and needs to be intercepted.
        var update = store.state.flag.update;
        if(update&&Date.now()-update<100){
            return;
        }
        handlePaster();
    });

/**
 * Handling copied content
 */
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 = "<br/>";
    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);
    
}


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

7. カーソルを移動させる 要素を挿入した後にノードの位置を変更した場合、カーソルは失われるため、要素を挿入した後に移動させる必要があります。

/**
 * emoji into unicode storage, \ud83c\udf4f
 * Then innerHTMl="\ud83c\udf4f" to display the emoji
 * @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 conversion to solid characters for background storage
 * unicode2Enti("\ud83c\udf4f") ---" "&#127823;"
 * then innerHTMl="&#127823;" to display the expression
 * @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) {
          // auxiliary plane characters (the class we need to do the processing)
          H = char.charCodeAt(0); // take out the high bit
          L = char.charCodeAt(1); // fetch the low bit
          code = (H - 0xd800) * 0x400 + 0x10000 + L - 0xdc00; // conversion algorithm
          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; }  
              return true;  
          } else if (0x3297 <= hs && hs <= 0x3299) { return true; } else if (0x3297 <= hs && hs <= 0x3299) { return true; }  
              return true;  
          } else if (hs == 0xa9 || hs == 0xae || hs == 0x303d || hs == 0x3030  
                  || hs == 0x2b55 || hs == 0x2b1c || hs == 0x2b1b  
                  || hs == 0x2b50) {  
              return true;  
          }  
      }  
  }  
}  

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;
        }
        //If there is already an update, and the time is within 100ms, it is considered that textchange, then paste, which is not the standard paste and needs to be intercepted.
        var update = store.state.flag.update;
        if(update&&Date.now()-update<100){
            return;
        }
        handlePaster();
    });

9. 貼り付ける内容を取得する。以前、ファイルの中身を直接取得しようとしたのは愚の骨頂でした。 event.clipboardData.items[0].getAsString()ではダメでした、ダメだったんです。

/**
 * Handling copied content
 */
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 = "<br/>";
    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 into unicode storage, \ud83c\udf4f
 * Then innerHTMl="\ud83c\udf4f" to display the emoji
 * @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 conversion to solid characters for background storage
 * unicode2Enti("\ud83c\udf4f") ---" "&#127823;"
 * then innerHTMl="&#127823;" to display the expression
 * @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) {
          // auxiliary plane characters (the class we need to do the processing)
          H = char.charCodeAt(0); // take out the high bit
          L = char.charCodeAt(1); // fetch the low bit
          code = (H - 0xd800) * 0x400 + 0x10000 + L - 0xdc00; // conversion algorithm
          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; }  
              return true;  
          } else if (0x3297 <= hs && hs <= 0x3299) { return true; } else if (0x3297 <= hs && hs <= 0x3299) { return true; }  
              return true;  
          } else if (hs == 0xa9 || hs == 0xae || hs == 0x303d || hs == 0x3030  
                  || hs == 0x2b55 || hs == 0x2b1c || hs == 0x2b1b  
                  || hs == 0x2b50) {  
              return true;  
          }  
      }  
  }  
}