需要开发一个移动端的富文本编辑器,但是不想用uedit等富文本编辑器,那就只能自己支持了。
1、contenteditable="true",对组件设置contenteditable="false",这俩是前提
<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">更改图片</div> </div> <div class="del_item" data-action="del">
<div></div> </div> </div>
2、placeholder,不要写在content里面,用样式empty来设置,否则插入组件之后,删除会自动清空。应该在外层使用div,用绝对定位,移动到你想要的位置。
.content:empty::before{ content: attr(placeholder); font-size: 14px; color:
#CCC; line-height: 21px; padding-top: 10px; } <div
class="placeholder">请输入不少于150字正文,支持图文商品混排哦!</div>
3、监听content的focus方法,当光标聚焦的时候,给元素插入default元素。这里开始琢磨了我一段时间,因为默认的插入文本都是在content里面,如输入aaaa:<div
id="content">aaaa</div>,然后回车<div
id="content">aaaa<div></div></div>,显然,如果是这种样式,不符合规定,且不好操作,之前一直想着替换,单问题很多:光标,回车不自动出现回车效果,而是里面嵌套。如下设置之后,则完全符合预期。
const defaultHtml = '<p class="feedback_mix_text citem"><br/></p>'; 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<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); }
4、最核心的,需要记录最后的光标位置。这里还判断了光标位置,如果当前光标不再content里面的text标签里面,则不需要记录,我这边输入内容都会在<p
class="feedback_mix_text citem"></p>里面。
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;
//保护,防止出现插入内容不是在<p></p>里面,那么则不需要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 = "<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转换成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; } } } }
热门工具 换一换