【问题标题】:Trying to modify the text of a document onkeydown is inserting text incorrectly尝试修改文档的文本 onkeydown 错误地插入文本
【发布时间】:2025-11-26 08:55:01
【问题描述】:

我有以下html:

<html>
 <head>
   <script>

function myKeyDown()
{
    var myDiv = document.getElementById('myDiv');
    myDiv.innerHTML = myDiv.innerHTML.replace(/(@[a-z0-9_]+)/gi, '<strong>$1</strong>');
}

function init()
{
    document.addEventListener('keydown', myKeyDown, false);
    document.designMode = "on";
}

window.onload = init;
   </script>
 </head>
 <body>
   <div id="myDiv">
     This is my variable name: @varname. If I type here things go wrong...
   </div>
 </body>
</html>

我的目标是在编辑时做一种语法高亮,高亮以@符号开头的变量名。但是,当我编辑文档的正文时,该函数会运行,但在执行击键之前光标会自动移动到正文的开头。

我的假设是按键事件试图在指定的索引处插入新字符,但是当我运行替换函数时,索引变得混乱,因此它默认字符插入点位于开头。

顺便用火狐来测试。

任何帮助将不胜感激。

谢谢, B.J.

【问题讨论】:

    标签: javascript html


    【解决方案1】:

    你的假设是正确的。使用innerHTML 进行替换意味着浏览器必须丢弃 div 中的所有节点并从您提供的 HTML 字符串创建新节点,这意味着之前存在的选择的节点不再存在。这是非常低效的,尤其是在每次按键时都这样做。您需要改用 DOM 方法来执行此操作。

    我建议在进行任何替换之前等待一段时间的键盘不活动,而不是在每次按键后都这样做。

    最后,这需要在哪些浏览器中工作?

    更新

    我认为这是一个有趣的问题,所以我写了一个完整的解决方案。我也改变了使用innerHTML 的想法。复杂的一点是您必须使用完全不同的方法从所有其他浏览器中保存和恢复 IE 中的选择。我还将它更改为使用contenteditable 而不是designMode。这排除了 Firefox 2,但它简化了一些事情,所以我希望没问题。

    <script type="text/javascript">
    function getBoundary(el, textNodes, charIndex) {
        var charsSoFar = 0, textNodeLength;
    
        // Walk text nodes
        for (var i = 0, len = textNodes.length; i < len; ++i) {
            textNodeLength = textNodes[i].data.length;
            charsSoFar += textNodeLength;
            if (charsSoFar >= charIndex) {
                return {
                   node: textNodes[i],
                   offset: charIndex + textNodeLength - charsSoFar
                };
            }
        }
    
        throw new Error("Boundary not found");
    }
    
    function highlightVars() {
        var myDiv = document.getElementById('myDiv');
        var selectedRange, range, divText;
        var selectionStartPos, selectionLength;
    
        var hasRanges = !!(window.getSelection
            && document.createRange);
        var hasTextRanges = !!(document.selection
            && document.body.createTextRange);
    
        // Get the selection text position within the div
        if (hasRanges) {
            selectedRange = window.getSelection().getRangeAt(0);
            range = document.createRange();
            range.selectNodeContents(myDiv);
            divText = range.toString();
            range.setEnd(selectedRange.startContainer, selectedRange.startOffset);
            selectionStartPos = range.toString().length;
            selectionLength = selectedRange.toString().length;
        } else if (hasTextRanges) {
            selectedRange = document.selection.createRange();
            range = document.body.createTextRange();
            range.moveToElementText(myDiv);
            divText = range.text;
            range.setEndPoint("EndToStart", selectedRange);
            selectionStartPos = range.text.length;
            selectionLength = selectedRange.text.length;
        }
    
        // Substitute in existing text with vars highlighted
        myDiv.innerHTML = divText.replace(/(@[a-z0-9_]+)/gi,
           '<strong>$1</strong>').replace(/ $/, "\u00a0");
    
        // Restore selection
        if (hasRanges) {
            var textNodes = [];
    
            for (var n = myDiv.firstChild; n; n = n.nextSibling) {
                textNodes.push( (n.nodeType == 1) ? n.firstChild : n );
            }
    
            var selectionStartBoundary = getBoundary(myDiv, textNodes,
                selectionStartPos);
            var selectionEndBoundary = getBoundary(myDiv, textNodes,
                selectionStartPos + selectionLength);
    
            range.setStart(selectionStartBoundary.node,
                selectionStartBoundary.offset);
            range.setEnd(selectionEndBoundary.node,
                selectionEndBoundary.offset);
    
            var sel = window.getSelection();
            sel.removeAllRanges();
            sel.addRange(range);
        } else if (hasTextRanges) {
            range.moveToElementText(myDiv);
            range.moveStart("Character", selectionStartPos);
            range.collapse();
            range.moveEnd("Character", selectionLength);
            range.select();
        }
    }
    
    var keyTimer;
    
    function myKeyDown() {
        if (keyTimer) {
            window.clearTimeout(keyTimer);
        }
        keyTimer = window.setTimeout(function() {
            keyTimer = null;
            highlightVars();
        }, 1000);
    }
    
    function init() {
        var myDiv = document.getElementById("myDiv");
        myDiv.onkeydown = myKeyDown;
    }
    
    window.onload = init;
    </script>
    
    <body>
       <div id="myDiv" contenteditable="true">This is my variable
       name: @varname. If I type here things go wrong...</div>
    </body>
    

    【讨论】:

    • 我需要它至少适用于 IE 和 Firefox,但最好是兼容所有浏览器。不过,我不确定您使用 DOM 方法是什么意思。我需要这是一种语法高亮,意味着在用户键入特定单词后立即更改颜色或其他任何内容。
    • 那是一段非常令人印象深刻的代码。除了一个小例外,它可以完美运行:如果您键入一串文本,末尾有一个额外的空格,然后键入另一个字符,它会用新字符覆盖末尾的空格。我认为这与 document.selection 不包括尾随空格有关。有什么想法吗?
    • 我认为这与 Firefox 对 innerHTML 的实现有关。我已对其进行了更改,以便将任何尾随空格替换为不间断空格并且问题已解决。
    • 我发现的另一个问题与插入换行符有关。如果您在字符串的末尾插入换行符,它会起作用,但如果您尝试在该换行符之后添加任何内容,它会认为光标位于换行符之前的行尾。
    • 嗯。这类问题是我一开始不建议使用innerHTML 的原因。我会玩的。