SOURCE

console 命令行工具 X clear

                    
>
console
const editorEl = document.querySelector("#editor");
let html = "";

function initEditorContent(textContent) { }

// 禁用回车
editorEl.addEventListener("keydown", (e) => {
    if (e.key === "Enter") {
        e.preventDefault();
        return;
    }

    if (e.key === "Backspace") {
        // 获取当前选区
        const sel = window.getSelection();
        // 获取第一个 Range 对象
        const range = sel.getRangeAt(0);
        if (range.commonAncestorContainer.nodeName === "DIV") {
            // 获取光标所在的节点和位置
            const startNode = range.startContainer;
            const startOffset = range.startOffset;
            // 遍历节点,找到第一个 contenteditable="false" 的 span 元素
            let node = startNode;
            while (node && node.nodeType === Node.ELEMENT_NODE) {
                if (node.getAttribute("contenteditable") === "false") {
                    // 找到了正在删除的 span 元素
                    break;
                }
                node = node.childNodes[startOffset - 1];
            }
            if (node && node.nodeType === Node.ELEMENT_NODE) {
                e.preventDefault();
                editorEl.removeChild(node);
                computedInput();
            }
        }
    }
});

// 用户输入内容
editorEl.addEventListener("input", (e) => {
    const { inputCount } = computedInput();
    // 如果输入的有效字符达到上限,并且本次操作是增加字符,则阻止用户
    if (inputCount > 50 && e.target.innerHTML.length > html.length) {
        e.preventDefault();
        e.target.innerHTML = html;
        setFocus(editorEl);
        computedInput();
        return false;
    }

    html = e.target.innerHTML;
});

editorEl.addEventListener("paste", (e) => {
    e.preventDefault();
});

function computedInput() {
    const textContent = editorEl.textContent;
    const symbolReg = /\【(.*?)\】/g;
    const symbolMatch = textContent.match(symbolReg) || [];
    // symbol字符实际的字符数
    const symbolLength = symbolMatch.reduce(
        (pre, item) => pre + item.length,
        0
    );

    const textContentLength = textContent.length;
    const charLength = textContentLength - symbolLength;

    editorEl.setAttribute(
        "data-input-count",
        symbolMatch.length * 5 + charLength
    );

    return {
        symbolMatch,
        charLength,
        inputCount: symbolMatch.length * 5 + charLength,
    };
}

function insertSymbol(symbol) {
    const sel = window.getSelection();
    // 获取选区对象
    let range = sel.getRangeAt(0);
    // 若选中区域不是编辑框或不是编辑框的子节点,则终止操作
    if (
        range.commonAncestorContainer !== editorEl &&
        !editorEl.contains(range.commonAncestorContainer)
    ) {
        return;
    }

    // 删除选中的内容
    range.deleteContents();
    const el = document.createElement("div");
    el.innerHTML = `<span class="symbol" contenteditable="false">${symbol}</span>`;
    let frag = document.createDocumentFragment();
    let node;
    let lastNode;
    while ((node = el.firstChild)) {
        lastNode = frag.appendChild(node);
    }
    range.insertNode(frag);
    if (lastNode) {
        range = range.cloneRange();
        range.setStartAfter(lastNode);
        range.collapse(true);
        sel.removeAllRanges();
        sel.addRange(range);
    }
    setFocus(editorEl);

    const { inputCount } = computedInput();
    // 如果输入的有效字符达到上限,并且本次操作是增加字符,则阻止用户
    if (inputCount > 50) {
        editorEl.innerHTML = html;
        setFocus(editorEl);
        computedInput();
        return;
    }

    html = editorEl.innerHTML;
}

function setFocus(el) {
    el.focus();
    var range = document.createRange();
    range.selectNodeContents(el);
    range.collapse(false);
    var sel = window.getSelection();
    //判断光标位置,如不需要可删除
    if (sel.anchorOffset != 0) {
        return;
    }
    sel.removeAllRanges();
    sel.addRange(range);
}
<h3>短信模板编辑器</h3>
<ol>
	<li>点击按钮可以在光标处插入变量</li>
	<li>可以在编辑框内输入文本内容</li>
</ol>
<p>已知问题</p>
<ol>
	<li>变量和输入的普通文本混杂时,删除变量可能会按两下,删除文本可能会连续删除</li>
</ol>
<div id="editor" data-input-count="0" contenteditable="true"></div>
<button onclick="insertSymbol('【姓名】')">【姓名】</button>
<button onclick="insertSymbol('【手机号码】')">【手机号码】</button>
<button onclick="insertSymbol('【年龄】')">【年龄】</button>
#editor {
    position: relative;
    width: 400px;
    height: 200px;
    border: 1px solid #000;
    padding: 20px;
}

#editor::after {
    position: absolute;
    right: 5px;
    bottom: 5px;
    content: attr(data-input-count) "/50";
}

.symbol {
    user-select: none;
    color: orangered;
}