SOURCE

console 命令行工具 X clear

                    
>
console
const simEditor = (function () {
    /**
     * 处理函数
     * @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
    }

    /**
     * 缓存光标
     * @param {Node} editNode 编辑节点
     */
    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 html2text(html) {
        if (!html) {
            return ''
        }
        const enterRegExp = /<\/(p|div)>/g
        // img标签alt的值
        const altRegExp = /<img.*?alt="(.*?)">/ig
        const text = html
            .replace(enterRegExp, '\n')
            .replace(/\\r/g, '')
            .replace(altRegExp, '$1')
            .replace(/<[^>]+>/g, '')
        return text.trim()
    }

    /**
     * 文本渲染过滤器
     * @param {string} text 文本
     * @param {{[alt:string]:[src:string]}} txt2imgMap 文本2图片
     * @returns {string}
     */
    const mgsFilters = (text, txt2imgMap) => {
        txt2imgMap = txt2imgMap || {
            '[猫猫]': 'https://img.yzcdn.cn/vant/cat.jpeg'
        }
        return replaceEmotion(text, txt2imgMap, ({ src, alt }) => {
            return `<img src="${src}" alt="${alt}" />`
        })
    }

    /**
     * 插入信息到编辑器
     * @param {Node} node 节点
     * @param {string} html html文本
     */
    function insertHtml(node, html) {
        const content = document.createElement('p')
        content.innerHTML = html || '<br>'
        node.innerHTML = ''
        node.appendChild(content)
    }

    /**
     * 获取粘贴文本
     * @param {Event} e 粘贴事件
     * @returns {string}
     */
    function getPasteText(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')
        }
        return pasteText
    }

    /**
     * 剔除编辑区容器的 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
    }

    /**
   * 插入元素到编辑器
   * @param {Node} editNode 编辑节点对象
   * @param {Node|string} insertNode  插入节点对象
   * @param {{enterNodeName:string,isEnter:boolean,insertType:string}} op 选项 enterNodeName:换行标签,大写,isEnter 是否是换行,insertType:插入类型text/html
   */
    function insertNodeAtCaret(editNode, insertNode, op = {}) {
        const { enterNodeName = 'p', isEnter = false } = op
        let sel, range, inEditRange
        if (window.getSelection) {
            sel = window.getSelection()

            if (sel.getRangeAt && sel.rangeCount) {
                range = sel.getRangeAt(0)
                inEditRange = range && (range.endContainer === editNode || editNode.contains(range.endContainer))
            }

            // 取出缓存range
            if (!inEditRange && editNode._cache_range) {
                range = editNode._cache_range
                inEditRange = range && (range.endContainer === editNode || editNode.contains(range.endContainer))
            }

            if (!inEditRange) {
                const editRange = document.createRange()
                editRange.selectNode(editNode)
                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

            // 只有父节点是否是编辑对象
            const isPEditNode = range.endContainer.parentNode === editNode

            let endContainer = range.endContainer
            // 在文本中,需要把范围设置到P标签上
            if (!isPEditNode && isInText) {
                endContainer = range.endContainer.parentNode
            }

            // 只是换行,换行内容存在'<br>'
            const justBr = endContainer.innerHTML === '<br>'

            // 编辑器是否空
            const isEmptyEditNode = !editNode.innerHTML

            if (isEnter) {
                if (isEmptyEditNode) { // 存在空元素
                    range.insertNode(insertNode)
                    range.setStartAfter(insertNode)
                    insertNode = insertNode.cloneNode(true)
                } else if (isInText) { // 在文本
                    const textLen = range.endContainer.textContent.length
                    const parentNodes = range.endContainer.parentNode.childNodes
                    const parentNodeLen = parentNodes.length
                    const index = [...parentNodes].indexOf(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) { // 在换行标签里
                    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 isBlockInsertNode = ['P', 'DIV'].indexOf(insertNode.nodeName) > -1
                // 当前在块元素里
                if (isInEnter) {
                    if (isBlockInsertNode) {
                        // 当前空行只有br内容
                        if (justBr) {
                            range.endContainer.innerHTML = insertNode.innerHTML
                            range.setEnd(range.endContainer, range.endContainer.childNodes.length)
                            insertNode = null
                        } else {
                            range.setStartAfter(endContainer)
                        }
                    }
                } else {
                    if (!isBlockInsertNode && !isInText) {
                        const _p = document.createElement('p')
                        _p.appendChild(insertNode)
                        insertNode = _p
                    }
                }
            }

            // 插入元素
            if (insertNode) {
                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;
        }
    }

    const funcs = {
        insertNodeAtCaret,
        observeNode,
        mutationsFilter,
        getPasteText,
        mgsFilters,
        insertHtml,
        replaceEmotion
    }
    /**
     * 初始化编辑器
     * @param {Node} node 编辑器节点
     * @param {{onChange:(txt)=>void,onPaste:(e:Event)=>void|boolean,textFilters:(text:string)=>string}} op 选项,
     * onChange:变化监听,onPaste:粘贴事件,textFilters,自定义文本过滤
     * @returns {funcs}
     */
    function initEdit(node, op = {}) {
        if (initEdit) {
            // return
        }
        const onChange = op.onChange
        const onPaste = op.onPaste
        const textFilters = op.textFilters || mgsFilters

        // 改变光标缓存
        function _editChangeCacheRange() {
            editCacheRange(node)
        }

        // 存在光标就清空,并重新记录新光标
        ['keyup', 'focus', 'click'].forEach((key) => {
            node.removeEventListener(key, _editChangeCacheRange)
            node.addEventListener(key, _editChangeCacheRange)
        })

        const _pasteEvent = (e) => {
            e.preventDefault()
            let text = onPaste && onPaste(e)
            if (!text) {
                text = getPasteText(e)
                text = textFilters(html2text(text))
            }
            if (text) {
                // 创建内容模板元素
                const content = document.createElement('template')
                content.innerHTML = text
                // 只要内容片段就行DocumentFragment
                insertNodeAtCaret(node, content.content)
            }
            return false
        }

        // 拦截粘贴
        node.removeEventListener('paste', _pasteEvent)
        node.addEventListener('paste', _pasteEvent)

        // 监听节点
        observeNode(node, {
            onChange(mutationsList, observer) {
                // const datas = mutationsFilter(mutationsList, observer.target)
                // 空的时候保留一个<p><br><p>
                const isEmpty = !node.innerHTML
                if (isEmpty) {
                    node.innerHTML = ''
                    const br = document.createElement('p')
                    br.innerHTML = '<br>'
                    insertNodeAtCaret(node, br)
                } else {
                    // trim() 去掉头尾的空白符,换行、空格、制表符等
                    if (onChange) {
                        const text = html2text(node.innerHTML)
                        onChange(text)
                    }
                    // 存光标这步很关键
                }
                editCacheRange(node)
            }
        })

        /**
       * 插入元素到编辑器
       * @param {HTMLElement} insertNode  插入节点对象
       * @param {boolean} isEnter 是否是换行
       * @param {string} enterNodeName 换行标签,大写
       */
        const _insertNodeAtCaret = (
            insertNode,
            op = {}) => {
            return insertNodeAtCaret(node, insertNode, op)
        }

        const _insertHtml = (html) => {
            const _html = textFilters(html2text(html))
            return insertHtml(node, _html)
        }

        const _funcs = {
            ...funcs,
            insertNodeAtCaret: _insertNodeAtCaret,
            insertHtml: _insertHtml,
            html: _insertHtml
        }

        return _funcs
    }

    return {
        ...funcs,
        initEdit
    }
})()



const inEnter = document.querySelector('#inEnter');
const inImg = document.querySelector('#inImg');
const inText = document.querySelector('#inText');

const showMsg = document.querySelector('#showMsg');
const showText = document.querySelector('#showText');
const edit = document.querySelector('.edit')

let mgsValue = '输入点什么...������\n哈哈哈哈[猫猫]'

function setMsg(text) {
    showText.innerHTML = text
    showMsg.innerHTML = simEditor.mgsFilters(text)
}

const editObj = simEditor.initEdit(edit, {
    onChange: (text) => {
        setMsg(text)
    }
})
// 初始化内容
editObj.html(mgsValue)


inEnter.addEventListener('click', () => {
    const br = document.createElement('p')
    br.innerHTML = '<br>'
    // const br = document.createTextNode('\n')  // 纯\n换行
    // const br = document.createElement('br') // 纯br换行
    editObj.insertNodeAtCaret(br, { isEnter: true })
})

inText.addEventListener('click', () => {
    const text = document.createTextNode('哈哈')
    editObj.insertNodeAtCaret(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 = '[猫猫]';
    editObj.insertNodeAtCaret(img)
})
<div>信息显示器:</div>
<div class="show-msg" id="showMsg"></div>
<div>纯文本:</div>
<div class="show-txt" id="showText"></div>


<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> -->
body {
    background: #fff;
}

.edit {
    width: 200px;
    padding: 10px;
    margin-top: 20px;
    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
}