You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在contenteditable的div中将单词包裹在span内并保留光标位置?

问题

如何在设置了contenteditable属性的<div>中,将每个单词包裹在<span>元素内,同时保留光标位置?
示例效果:用户输入the fox guy jumped over the dog时,最终DOM结构应为:

<span>the</span> <span>fox</span> <span>guy</span> <span>jumped</span> <span>over</span> <span>the</span> <span>dog</span>

已尝试方案

现有代码如下,问题是修改innerHTML后光标会直接跳转到内容末尾:

document.querySelector("#container").addEventListener("input", function() {
    this.innerHTML = "<span>" + this.innerText
        .replaceAll("&", "&amp;") // 转义HTML特殊字符
        .replaceAll("&lt;", "&amp;lt;")
        .replaceAll("&gt;", "&amp;gt;")
        .split(" ")
        .join("</span><span>") + "</span>";
});

补充说明

需要使用input事件实现功能,要在用户输入过程中实时拆分单词,不能等编辑区域失焦才触发。

解决方案

核心思路是:修改DOM前先记录光标相对于纯文本内容的偏移量,DOM更新完成后再把光标还原到对应的偏移位置即可,完整实现代码如下:

const editableDiv = document.querySelector("#container");

// 工具函数:获取光标相对于可编辑元素纯文本的偏移量
function getTextCaretOffset(el) {
    const selection = window.getSelection();
    let offset = 0;
    if (selection.rangeCount > 0) {
        const range = selection.getRangeAt(0);
        const preCaretRange = range.cloneRange();
        preCaretRange.selectNodeContents(el);
        preCaretRange.setEnd(range.endContainer, range.endOffset);
        offset = preCaretRange.toString().length;
    }
    return offset;
}

// 工具函数:根据纯文本偏移量还原光标位置
function setTextCaretOffset(el, targetOffset) {
    const selection = window.getSelection();
    const range = document.createRange();
    range.setStart(el, 0);
    let currentOffset = 0;
    const nodeStack = [el];
    let node, found = false;

    while (node = nodeStack.pop()) {
        if (node.nodeType === Node.TEXT_NODE) {
            const nodeTextLength = node.length;
            // 判断目标偏移是否在当前文本节点范围内
            if (targetOffset >= currentOffset && targetOffset <= currentOffset + nodeTextLength) {
                range.setStart(node, targetOffset - currentOffset);
                range.collapse(true);
                found = true;
                break;
            }
            currentOffset += nodeTextLength;
        } else {
            // 倒序压入子节点,遍历时顺序和DOM顺序一致
            let i = node.childNodes.length;
            while (i--) {
                nodeStack.push(node.childNodes[i]);
            }
        }
    }

    if (found) {
        selection.removeAllRanges();
        selection.addRange(range);
    }
}

// 输入事件监听
editableDiv.addEventListener("input", function() {
    // 1. 先记录修改前的光标偏移
    const caretOffset = getTextCaretOffset(this);
    // 2. 执行单词包裹逻辑,注意这里加了空格,避免单词间空格丢失
    this.innerHTML = "<span>" + this.innerText
        .replaceAll("&", "&amp;")
        .replaceAll("<", "&lt;")
        .replaceAll(">", "&gt;")
        .split(" ")
        .join("</span> <span>") + "</span>";
    // 3. 恢复光标到原位置
    setTextCaretOffset(this, caretOffset);
});

说明

  • 代码针对原有逻辑做了小调整,拼接span时加入了空格,避免单词之间的空格被吞掉
  • 偏移量计算完全基于纯文本长度,不受包裹的span标签影响,光标定位准确
  • 全程基于input事件触发,满足实时处理的需求

内容的提问来源于stack exchange,提问作者illogicalapple

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.24 22:06:03