在可编辑的div中设置光标位置

8

概述:

我正试图实现这样一种效果,即当用户在可编辑的 div 中键入 ([ 时,自动插入第二个 )],并将光标定位在两者之间,也就是在 () 之间。


JSFIDDLE代码链接

-- 的右边输入并查看第一行如何工作,而第二行则不起作用。


我的努力:

我正在使用这段代码(由Tim Down提供)来突出显示文本的某些部分并设置光标位置。前者有效,但后者无效 :(

function getTextNodesIn(node) { // helper
    var textNodes = [];
    if (node.nodeType == 3) {
        textNodes.push(node);
    } else {
        var children = node.childNodes;
        for (var i = 0, len = children.length; i < len; ++i) {
            textNodes.push.apply(textNodes, getTextNodesIn(children[i]));
        }
    }
    return textNodes;
}

function highlightText(el, start, end) { // main
    if (el.tagName === "DIV") { // content-editable div
        var range = document.createRange();
        range.selectNodeContents(el);
        var textNodes = getTextNodesIn(el);
        var foundStart = false;
        var charCount = 0,
            endCharCount;

        for (var i = 0, textNode; textNode = textNodes[i++];) {
            endCharCount = charCount + textNode.length;
            if (!foundStart && start >= charCount && (start < endCharCount || (start == endCharCount && i < textNodes.length))) {
                range.setStart(textNode, start - charCount);
                foundStart = true;
            }
            if (foundStart && end <= endCharCount) {
                range.setEnd(textNode, end - charCount);
                break;
            }
            charCount = endCharCount;
        }

        var sel = window.getSelection();
        sel.removeAllRanges();
        sel.addRange(range);
    } else { // textarea
        el.selectionStart = start;
        el.selectionEnd = end;
    }
}

注意:

  1. <div> 元素会有子元素(主要是 <br>)。
  2. 只需要使用原生JS支持Chrome浏览器。

我的问题:

  1. 为什么上述函数不起作用?
  2. 如何使其起作用?

我花了数小时寻找解决方法,但并没有找到太有用的信息。有些是关于在子 div 的开头或结尾进行设置,但对我而言,可以是任何位置,任何地方。

更新:

感谢所有人的帮助,终于完成了开发

2个回答

13

这里有一种更简单的方法。有几个需要注意的事项:

  • keypress 是唯一可以可靠检测已键入字符的按键事件。keyupkeydown 无法做到。
  • 该代码通过阻止 keypress 事件的默认操作来手动处理括号/大括号的插入。
  • 使用 DOM 方法时,选择/光标等内容很容易实现。
  • 这在 IE <= 8 中无法工作,因为它们具有不同的范围和选择 API。如果您需要支持这些浏览器,建议使用我的自己的Rangy库。虽然没有使用它也是可能的,但我真的不想写额外的代码。

演示:

http://jsfiddle.net/HPeb2/

代码:

var editableEl = document.getElementById("editable");
editableEl.addEventListener("keypress", function(e) {
    var charTyped = String.fromCharCode(e.which);
    if (charTyped == "{" || charTyped == "(") {
        // Handle this case ourselves
        e.preventDefault();

        var sel = window.getSelection();
        if (sel.rangeCount > 0) {
            // First, delete the existing selection
            var range = sel.getRangeAt(0);
            range.deleteContents();

            // Insert a text node at the caret containing the braces/parens
            var text = (charTyped == "{") ? "{}" : "()";
            var textNode = document.createTextNode(text);
            range.insertNode(textNode);

            // Move the selection to the middle of the inserted text node
            range.setStart(textNode, 1);
            range.setEnd(textNode, 1);
            sel.removeAllRanges();
            sel.addRange(range);
        }
    }
}, false);

有趣的解决方案。我会在2-3天内尝试实验一下。 - Gaurang Tandon
谢谢!这正是我所需的!另外,你有什么建议/答案来克服我所拥有的getCaretPosition函数的限制吗? - Gaurang Tandon

5
为了实现您在摘要中所述的目标,请尝试更改当前光标位置处的节点值。由于您的代码已附加到keyup事件,因此可以确保范围已经折叠并且位于文本节点上(所有这些都发生在按键时,该事件已经触发)。
function insertChar(char) {
    var range = window.getSelection().getRangeAt(0);
    if (range.startContainer.nodeType === Node.TEXT_NODE) {
        range.startContainer.insertData(range.startOffset, char);
    }
}

function handleKeyUp(e) {
    e = e || window.event;

    var char, keyCode;
    keyCode = e.keyCode;

    char =
        (e.shiftKey ? {
            "222": '"',
            "57":  ')',
            "219": '}'
        } : {
            "219": "]"
        })[keyCode] || null;

    if (char) {
        insertChar(char);
    }
}

document.getElementById("editable").onkeyup = handleKeyUp;

Fiddle

另外,我发现你在使用 innerHTML 设置新值时(在 Element.prototype.setText 中),这让我感到担忧!innerHTML 会完全删除容器中先前的任何内容。由于光标绑定到特定元素,而那些元素刚刚被删除,浏览器应该怎么办?如果您关心光标结束后的位置,请尽量避免使用它。

至于 highlightText 的问题,很难说为什么出了问题。您的 fiddle 没有显示它在任何地方被使用,我需要看到它的使用情况才能进一步诊断。但是,我有一个想法可能会出错:

我认为您应该仔细查看 getCaretPosition。您将其视为返回光标位置,但它并不是这样做的。请记住,对于浏览器来说,您的光标位置总是一个范围。它始终有一个起点和一个终点。有时,当范围折叠时,起点和终点是相同的点。然而,您可以获得光标位置并将其视为单个点的想法是一种危险的过度简化。

getCaretPosition 还有另一个问题。对于您可编辑的 div,它会执行以下操作:

  1. 选择新范围中的所有文本
  2. 将新范围的结束位置重置为光标的结束位置(因此选择了直到光标的结束位置的所有文本)。
  3. 调用 toString() 并返回结果字符串的长度。

正如您所指出的,某些元素(例如 <br />)会影响 toString() 的结果。某些元素(例如 <span></span>)则不会。为了正确计算这个值,您需要针对某些元素类型进行微调,而对于其他元素则不需要。这将变得混乱和复杂。如果您想要一个可以喂入 highlightText 并按预期工作的数字,则当前的 getCaretPosition 不太可能有帮助。

相反,我认为您应该尝试直接使用光标的起始点和结束点作为两个单独的位置,并相应地更新 highlightText。放弃当前的 getCaretPosition,直接使用浏览器的本机概念 range.startContainerrange.startOffsetrange.endContainerrange.endOffset


这是一个非常有见地的答案!谢谢你的指导!我真的很惊讶,所有这些繁重的方法中最好的解决方案竟然只是 range.startContainer o.O 我想我需要在这方面多读一些。再次感谢!!! - Gaurang Tandon
但是,您能解释一下 if (range.startContainer.nodeType === Node.TEXT_NODE) { 的必要性吗?还有 .insertData 是什么?抱歉,我找不到它们的文档。谢谢 :) - Gaurang Tandon
另外,我如何在可编辑的 div 中任意定位光标?(无关此括号自动完成功能)我已经在 Google 上搜索了过去两页,但没有结果。 - Gaurang Tandon
1
实际上,<br>元素不会对范围的toString()方法返回的字符串产生贡献:只有文本节点中的文本被包括在内。然而,在某些浏览器中,Selection.toString()并非如此。从getCaretPosition()中获取的原始答案清楚地列出了其限制。我完全同意您关于谨慎地进行DOM操作使得这比使用innerHTML更容易的所有观点。 - Tim Down
@GaurangTandon:所需的相关API(DOM RangeSelection)已经被提到:任何你得到的答案都将基于它们。insertDataDOM文本节点的一个方法。顺便说一下,我通过谷歌搜索“mdn insertdata”得到了最后一个链接。 - Tim Down
@TimDown 谢谢你提供这些资源!我正在浏览它们。顺便说一下,我犯的错误是只搜索了 insertData,所以我得到了所有的 SQL 和 w3schools。 - Gaurang Tandon

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接