console
const inEnter = document.querySelector('#inEnter');
const inImg = document.querySelector('#inImg');
const inText = document.querySelector('#inText');
const edit = document.querySelector('.edit')
/**
* 处理函数
* @callback ReplacerCallback
* @param {{src: string,alt:string}} e
*/
/**
* 表情替换
* @param {string} content 文本内容
* @param {Object} srcMap 表情映射
* @param {ReplacerCallback} replacer 处理函数
* @returns
*/
function replaceEmotion(
content,
srcMap,
replacer = (e) => {
return e;
}
) {
const pattern = /\[[\u4e00-\u9fa5_a-zA-Z]+\]/g;
let emotions = content.match(pattern);
emotions = Array.from(new Set(emotions))
let str = content;
str = str.replace(
pattern,
(s) => {
if (emotions.indexOf(s) > -1 && srcMap[s]) {
return replacer({ src: srcMap[s], alt: s })
} else {
return s
}
}
);
return str;
}
// 缓存光标
function editCacheRange(editNode) {
if (window.getSelection) {
const sel = window.getSelection();
if (sel.getRangeAt && sel.rangeCount) {
editNode._cache_range = sel.getRangeAt(0);
}
} else if (document.selection && document.selection.createRange) {
editNode._cache_range = document.selection.createRange();
}
}
// 改变光标缓存
function editChangeCacheRange() {
editCacheRange(edit)
}
// 存在光标就清空,并重新记录新光标
['keyup', 'focus', 'click'].forEach((key) => {
edit.removeEventListener(key, editChangeCacheRange)
edit.addEventListener(key, editChangeCacheRange)
})
// 暴力清除标签样式
function html2text(html) {
const enterRegExp = /<\/(p|div)>/g;
// img标签alt的值
const altRegExp = /<img.*?alt="(.*?)">/ig;
const text = html.replace(enterRegExp, '\n').replace(altRegExp, "$1").replace(/<[^>]+>/g, '')
return text.trim()
}
// 文本渲染过滤器
const mgsFilters = (text) => {
const txt2imgMap = {
'[猫猫]': 'https://img.yzcdn.cn/vant/cat.jpeg'
}
return replaceEmotion(text, txt2imgMap, ({ src, alt }) => {
return `<img src="${src}" alt="${alt}" />`
})
}
// 删除全部必须保留一个换行内容
function deleteEvent(e) {
if (e.keyCode !== 8 && e.keyCode !== 46) return
const isEmpty = !edit.innerHTML.replace(/<[^>]+>/g, '')
if (isEmpty) {
edit.innerHTML = ''
const br = document.createElement('p')
br.innerHTML = '<br>'
insertNodeAtCaret(edit, br, 1)
}
}
edit.removeEventListener('keydup', deleteEvent)
edit.addEventListener('keydup', deleteEvent)
// 插入信息到编辑器
function insertHtml(node, html) {
const text = mgsFilters(html2text(html))
const content = document.createElement('p')
content.innerHTML = text || '<br>'
edit.innerHTML = ''
node.appendChild(content)
}
// 粘贴事件拦截
function pasteEvent(e) {
if (!(e.clipboardData && e.clipboardData.items)) {
return;
}
const clipboardData = e.clipboardData // 暂不考虑 originalEvent 的情况
let pasteText = ''
if (clipboardData == null) {
pasteText = window.clipboardData && window.clipboardData.getData('text')
} else {
pasteText = clipboardData.getData('text/plain')
}
const text = mgsFilters(html2text(pasteText))
if (text) {
const content = document.createElement('p')
content.innerHTML = text
insertNodeAtCaret(edit, content)
}
e.preventDefault()
return false
}
edit.removeEventListener('paste', pasteEvent)
edit.addEventListener("paste", pasteEvent);
let mgsValue = ''
insertHtml(edit, mgsValue)
const showMsg = document.querySelector('#showMsg');
const showText = document.querySelector('#showText');
function setMsg(text) {
showText.innerHTML = text
showMsg.innerHTML = mgsFilters(text)
}
setMsg(mgsValue)
// // 监听输入
// edit.addEventListener('input', () => {
// console.log(edit)
// setMsg(edit.innerText)
// })
/**
* 剔除编辑区容器的 attribute 变化中的非 contenteditable 变化
* @param {MutationRecord[]} mutations
* @param {Node} tar 编辑区容器的 DOM 节点
*/
function mutationsFilter(mutations, tar) {
// 剔除编辑区容器的 attribute 变化中的非 contenteditable 变化
return mutations.filter(({ type, target, attributeName }) => {
return (
type != 'attributes' ||
(type == 'attributes' && (attributeName == 'contenteditable' || target != tar))
)
})
}
/**
* 节点变化监听
* @param {Node} node 节点
* @param {{onChange:MutationCallback}} op 选项
* @returns
*/
function observeNode(node, op = {}) {
const { onChange } = op;
// 观察器的配置(需要观察什么变动)
const config = {
subtree: true, // 子节点树变化
childList: true, // 节点变化
attributes: true, // 属性变化
attributeOldValue: true,
characterData: true, // 节点内容或文本变化
characterDataOldValue: true,
};
// 创建一个观察器实例并传入回调函数
const _nodeObserver = new MutationObserver((mutationsList, observer) => {
onChange && onChange(mutationsList, observer);
});
// 以上述配置开始观察目标节点
_nodeObserver.observe(node, config);
return _nodeObserver;
}
// 监听节点
observeNode(edit, {
onChange(mutationsList, observer) {
// const datas = mutationsFilter(mutationsList, observer.target)
const text = html2text(edit.innerHTML)
// trim() 去掉头尾的空白符,换行、空格、制表符等
setMsg(text)
// 存光标这步很关键
editCacheRange(edit)
}
})
/**
* 插入元素到编辑器
* @param {HTMLElement} editNode 编辑节点对象
* @param {HTMLElement} insertNode 插入节点对象
* @param {boolean} isEnter 是否是换行
* @param {string} enterNodeName 换行标签,大写
*/
function insertNodeAtCaret(editNode, insertNode, isEnter, enterNodeName = 'P') {
let sel, range;
if (window.getSelection) {
sel = window.getSelection();
if (sel.getRangeAt && sel.rangeCount) {
range = sel.getRangeAt(0);
}
// 取出缓存range
if (!range && editNode._cache_range) {
range = editNode._cache_range
sel.removeAllRanges();
sel.addRange(range);
inEditRange = true
}
// 做个搂底,把编辑器范围回正,并激活光标
const editRange = document.createRange()
editRange.selectNode(editNode)
let inEditRange = range && editRange.compareBoundaryPoints(Range.START_TO_END, range) > -1 && range.endContainer != editNode;
if (!inEditRange) {
range = editRange
range.selectNodeContents(editNode)
// 编辑器有内容就光标就移到最后
if (editNode.lastChild) {
range.selectNodeContents(editNode.lastChild)
range.setStart(editNode.lastChild, 1);
range.setEnd(editNode.lastChild, 1);
inEditRange = true
}
}
// 把选中范围清空
range.deleteContents();
// 是否在文本里
const isInText = range.endContainer.nodeName === '#text'
// 是否在换行标签里
const isInEnter = inEditRange && range.endContainer.nodeName === enterNodeName.toUpperCase()
// 光标范围里的元素
const rangeNodes = range.endContainer.childNodes
// 只是换行,换行内容存在'<br>'
const justBr = range.endContainer.innerHTML === '<br>'
if (isEnter) {
if (isInText) {
const textLen = range.endContainer.textContent.length
const parentNodes = range.endContainer.parentNode.childNodes
const parentNodeLen = parentNodes.length
const index = [...parentNodes].indexOf(range.endContainer)
// 文本只有父节点是否是编辑对象
const isPEditNode = range.endContainer.parentNode === editNode
let endContainer = range.endContainer.parentNode
if (isPEditNode) {
endContainer = range.endContainer
}
if (textLen <= range.endOffset && index == parentNodeLen - 1) {
range.setStartAfter(endContainer);
} else if (range.endOffset === 0 && index === 0) {
range.setEndBefore(endContainer);
} else {
insertNode = document.createTextNode('\n')
}
} else if (isInEnter) { // 是否在换行标签里
const textLen = range.endContainer.textContent.length
if (!justBr && range.endOffset === 0) {
range.setEndBefore(range.endContainer);
} else if (!justBr && rangeNodes.length > range.endOffset) {
// 在图片标签之间的换行
insertNode = document.createTextNode('\n')
} else {
// 把范围后移一个单位
range.setStartAfter(range.endContainer);
}
}
} else {
// 只是换行,换行内容存在'<br>'
if (justBr) {
// 不是换行,但是存在换行标签,把标签移除
range.endContainer.innerHTML = '';
}
}
const isEmptyEditNode = !editNode.innerHTML
// 给第一个元素添加
if (isEmptyEditNode) {
if (isEnter) {
range.insertNode(insertNode);
range.setStartAfter(insertNode);
insertNode = insertNode.cloneNode(true)
} else {
const _d = document.createElement(enterNodeName || 'p')
_d.appendChild(insertNode)
insertNode = _d
}
}
// 插入元素
range.insertNode(insertNode);
// 换行特殊处理
if (isEnter || isEmptyEditNode) {
range.selectNodeContents(insertNode);
}
// 清空范围,但并不删除内容
range.collapse(false);
sel.removeAllRanges();
sel.addRange(range);
// // 存一下光标
// edit._cache_range = range
} else if (document.selection && document.selection.createRange) {
// 吧啦吧啦吧啦兼容没有做
// range = document.selection.createRange()
// range.text = text;
}
}
inEnter.addEventListener('click', () => {
const br = document.createElement('p')
br.innerHTML = '<br>'
// const br = document.createTextNode('\n') // 纯\n换行
// const br = document.createElement('br') // 纯br换行
insertNodeAtCaret(edit, br, 1)
})
inText.addEventListener('click', () => {
const text = document.createTextNode('哈哈')
insertNodeAtCaret(edit, text)
})
inImg.addEventListener('click', () => {
const img = document.createElement('img');
img.src = 'https://img.yzcdn.cn/vant/cat.jpeg';
// img.src = 'data:image/gif;base64,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'
img.alt = '[猫猫]';
insertNodeAtCaret(edit, img)
})
<div contenteditable="true" tabindex="1" class="edit"></div>
<div class="cc">
<button id="inEnter">插入换行</button>
<button id="inText">插入哈哈</button>
<button id="inImg">插入图片</button>
</div>
<!-- <div class="mgsbox">
<textarea name="msg" id="mgsTxt" cols="50" rows="5"></textarea>
</div> -->
<div>信息显示器:</div>
<div class="show-msg" id="showMsg"></div>
<div>纯文本:</div>
<div class="show-txt" id="showText"></div>
body {
background: #fff;
}
.edit {
width: 200px;
padding: 10px;
white-space: pre-wrap;
background-color: #f5f5f5
}
.edit:focus {
outline: none
}
.edit img {
vertical-align: text-bottom;
padding: 0 1px;
width: 50px;
height: 50px;
}
.edit p {
margin: 0;
}
.cc {
padding: 20px 0
}
.show-msg {
width: 200px;
min-height: 40px;
padding: 10px;
border-radius: 6px;
white-space: pre-wrap;
color: #fff;
background-color: #3EB575
}
.show-msg img {
vertical-align: text-bottom;
padding: 0 1px;
width: 50px;
height: 50px;
}
.show-txt {
width: 200px;
min-height: 40px;
padding: 10px;
border-radius: 6px;
white-space: pre-wrap;
color: #fff;
background-color: #999
}