SOURCE

console 命令行工具 X clear

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