contenteditable实现编辑器,光标、输入法处理,emoji的显示和转换存储
需要开发一个移动端的富文本编辑器,但是不想用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;
}
}
}
}
上一篇: 自定义range样式 input[type=range]
下一篇: Linux中对文件的常规操作整理