如何区分 Firefox 中换行文本的第一个和最后一个位置

rto*_*tom 9 html javascript css

我正在处理一个contentEditable span想要将position: absolute元素放置在与光标同一行的位置。当文本因不适合而被换行时,就会出现问题 - 换行行的第一个和最后一个位置有奇怪的行为。

对于它们两者,当我位于第二行中的第一个位置时,偏移ygetBoundingClientRect()等于第一行的偏移量,但是如果我在第二行上进一步移动一个位置,则y offset正确匹配第二行。

在下面的代码片段中,显示了 Firefox 的此行为。对于 Chrome 来说,它似乎工作得很好,尽管在我的完整实现中它也有不精确的行为,但我能够为 chrome 解决它。然而,对于 Firefox,第一行的最后一个位置等于offset第一行,第二行的第一个位置等于offset等于第一行,之后就可以正常工作了。

在示例中,转到第一行的最后一个位置,并注意CURRENT_TOP控制台中的值显示16。如果你向右移动一个地方,光标已经在下一行,它仍然显示16。如果你再向右移动一个,它会说36

const textEl = document.getElementById("myText")

textEl.addEventListener("keyup", (event) => {
  const domSelection = window.getSelection();
  if (domSelection && domSelection.isCollapsed && domSelection.anchorNode) {
    let offsetNewLine = 0;

    const domRange = domSelection.getRangeAt(0);
    const rect = domRange.getBoundingClientRect();
    const rects = domRange.getClientRects();
    const newRange = document.createRange();
    const newRangeNextOffset = domSelection.anchorNode.textContent.length < domSelection.anchorOffset + 1 ? domSelection.anchorOffset : domSelection.anchorOffset + 1

    newRange.setStart(domSelection.anchorNode, newRangeNextOffset);
    newRange.setEnd(domSelection.anchorNode, newRangeNextOffset);
    const nextCharacterRect = newRange.getBoundingClientRect();

    console.log(`CURRENT_TOP: ${rect.y}, NEXT_CHAR_TOP: ${nextCharacterRect.y}`);
  }
})
Run Code Online (Sandbox Code Playgroud)
.text-container {
  width: 500px;
  display: inline-block;
  border: 1px solid black;
  line-height: 20px;
  padding: 5px;
}
Run Code Online (Sandbox Code Playgroud)
<span id="myText" class="text-container" contentEditable="true">Go on the last position in the first row and come it to first position in the second row</span>
Run Code Online (Sandbox Code Playgroud)

Kro*_*mot 5

先诊断,后治疗。

\n

诊断

\n

之所以会出现这种奇怪的行为,是因为 Chrome 和 Firefox 对待换行符的方式似乎有所不同。在 Chrome 和 Firefox 中执行以下代码片段。唯一的区别是我添加了

\n
anchorOffset: ${domSelection.anchorOffset}\n
Run Code Online (Sandbox Code Playgroud)\n

到控制台输出。我们将在下面讨论结果。

\n

\r\n
\r\n
anchorOffset: ${domSelection.anchorOffset}\n
Run Code Online (Sandbox Code Playgroud)\r\n
const textEl = document.getElementById("myText")\n\ntextEl.addEventListener("keyup", (event) => {\n  const domSelection = window.getSelection();\n  if (domSelection && domSelection.isCollapsed && domSelection.anchorNode) {\n    let offsetNewLine = 0;\n\n    let domRange = domSelection.getRangeAt(0);\n    let rect = domRange.getBoundingClientRect();\n    const rects = domRange.getClientRects();\n    const newRange = document.createRange();\n    const newRangeNextOffset = domSelection.anchorNode.textContent.length < domSelection.anchorOffset + 1 ? domSelection.anchorOffset : domSelection.anchorOffset + 1\n\n    newRange.setStart(domSelection.anchorNode, newRangeNextOffset);\n    newRange.setEnd(domSelection.anchorNode, newRangeNextOffset);\n    let nextCharacterRect = newRange.getBoundingClientRect();\n\n    console.log(`anchorOffset: ${domSelection.anchorOffset}, CURRENT_TOP: ${rect.y}, NEXT_CHAR_TOP: ${nextCharacterRect.y}`);\n  }\n})
Run Code Online (Sandbox Code Playgroud)\r\n
.text-container {\n  width: 500px;\n  display: inline-block;\n  border: 1px solid black;\n  line-height: 20px;\n  padding: 5px;\n}
Run Code Online (Sandbox Code Playgroud)\r\n
\r\n
\r\n

\n

浏览器在不同的位置换行,但这不是重点。首先查看Chrome中的输出。请注意,插入符号直接跳到下一行,实际存在的空格已转换为换行符(NL),看起来是经典的回车加换行(CR+LF)形式。所以当 NL Chrome 看到光标后,就像人眼一样,已经在第 2 行了。

\n
\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
第 1 行最后一个非空格换行符第 2 行第一个非空格
\'t\' 位于偏移量 61 处偏移量 62 处的 NL\'p\' 位于偏移量 63 处
\n
\n

铬合金

\n

现在是火狐。插入符号跟随空格,然后跳到下一行。空间 (SP) 已被保留。然而插入的换行符还没有在偏移量计算中。此外,它仍然被视为第 1 行的一部分,即人眼看到光标在第 2 行,但 Firefox 看到的是第 1 行。为什么呢?

\n

因此,Firefox在第 1 行末尾迭代两次(SP 然后 NL),但仅增加偏移量一次(SP 和 NL 一起),并且还没有真正移动到第 2 行。所有这些都让这里的事情变得如此混乱。

\n
\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
第 1 行最后一个非空格换行符第 2 行第一个非空格
\'n\' 位于偏移量 73 处SPNL,均位于偏移 74\'t\' 位于偏移量 75 处
\n
\n

火狐浏览器

\n

治疗

\n

我目前能想到的唯一方法是检测浏览器并引入特定于 Firefox 的解决方法,因此要检查 Firefox,例如

\n
const isFirefox = typeof InstallTrigger !== \'undefined\';\n
Run Code Online (Sandbox Code Playgroud)\n

经过测试,在 Firefox 111 上仍然可以工作。

\n

因此,我们可以通过指示我们是否处于 Firefox 换行符中来解决这个问题。让我们首先添加一些全局变量:

\n
// whether we\'re in a (Firefox-)NL\nlet isNewline = false;\n// whether we\'re in Firefox\nconst isFirefox = typeof InstallTrigger !== \'undefined\';\n
Run Code Online (Sandbox Code Playgroud)\n

请注意,如果需要,也可以用于isNewline其他浏览器。接下来,我们将 Firefox 特定的换行添加到keyup处理程序中:

\n
/*\n* Check whether we\'re in Firefox and on the edge of a line.\n* At need easily extendable for other browsers.\n*/\nif(isFirefox && rect.y < nextCharacterRect.y)\n{\n    // caret is after the SP, i.e. we\'re in the NL-sequence\n    if(isNewline)\n    {\n        /*\n        * Hop straight to the next line by\n        * de facto enforcing a LF+CR.\n        */\n        domRange = newRange;\n        domSelection.getRangeAt(0);\n        rect = domRange.getBoundingClientRect();\n\n        // end of Firefox\' NL-sequence\n        isNewline = false;\n    }\n    // begin of Firefox\' NL-sequence, i.e. we hit the SP\n    else\n        isNewline = true;\n}\n
Run Code Online (Sandbox Code Playgroud)\n

这可以例如通过用于微调的选择方向检测来扩展。

\n

让我们将所有内容放在下面的代码片段中。请注意,domRange分别。rect变成let而不是const.

\n

\r\n
\r\n
<span id="myText" class="text-container" contentEditable="true">Go on the last position in the first row and come it to first position in the second row</span>
Run Code Online (Sandbox Code Playgroud)\r\n
const isFirefox = typeof InstallTrigger !== \'undefined\';\n
Run Code Online (Sandbox Code Playgroud)\r\n
// whether we\'re in a (Firefox-)NL\nlet isNewline = false;\n// whether we\'re in Firefox\nconst isFirefox = typeof InstallTrigger !== \'undefined\';\n
Run Code Online (Sandbox Code Playgroud)\r\n
\r\n
\r\n

\n

结论

\n

可能有一个更优雅、更复杂的解决方案,但目前它可以完成任务。本质上,我们通过在 Chrome 中强制执行一种换行符 \xc3\xa0 来修改 Firefox 的换行行为LF+CR。唯一剩下的区别是在实际换行之前行尾的额外空格,即在 Firefox 中我们仍然需要按两次按键才能进入下一行,而不是像 Chrome 中那样按一次按键。但这在这里无关紧要。否则,两个浏览器的行为现在是等效的。此外,如有必要,此解决方法可以轻松适用于其他浏览器。

\n
\n
致谢
\n

使用换行符变量的最后灵感来自@herrstrietzel的一篇文章,其中还讨论了考虑选择方向和鼠标交互的方法。

\n