提问人:rtom 提问时间:11/13/2023 最后编辑:Rory McCrossanrtom 更新时间:11/20/2023 访问量:458
如何在Firefox中区分换行文本的第一个和最后一个位置
How to differentiate first and last position of wrapped text in firefox
问:
我正在处理一个我想将元素与光标放在同一行的地方。当文本因为不合适而被换行时,就会发生问题 - 换行的第一和最后一个位置具有奇怪的行为。contentEditable span
position: absolute
对于它们,当我在第二行的第一个位置时,偏移量等于第一行的偏移量,但是如果我在第二行上再移动一位,则与第二行正确匹配。y
getBoundingClientRect()
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}`);
}
})
.text-container {
width: 500px;
display: inline-block;
border: 1px solid black;
line-height: 20px;
padding: 5px;
}
<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>
答:
首先是诊断,然后是治疗。
诊断
这种奇怪的行为之所以发生,是因为Chrome和Firefox似乎以不同的方式对待换行符。在 Chrome 和 Firefox 中执行以下代码片段。唯一的区别是,我添加了
anchorOffset: ${domSelection.anchorOffset}
到控制台输出。我们将在下面讨论结果。
const textEl = document.getElementById("myText")
textEl.addEventListener("keyup", (event) => {
const domSelection = window.getSelection();
if (domSelection && domSelection.isCollapsed && domSelection.anchorNode) {
let offsetNewLine = 0;
let domRange = domSelection.getRangeAt(0);
let 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);
let nextCharacterRect = newRange.getBoundingClientRect();
console.log(`anchorOffset: ${domSelection.anchorOffset}, CURRENT_TOP: ${rect.y}, NEXT_CHAR_TOP: ${nextCharacterRect.y}`);
}
})
.text-container {
width: 500px;
display: inline-block;
border: 1px solid black;
line-height: 20px;
padding: 5px;
}
<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>
浏览器在这里换行在不同的位置,但这不是重点。首先查看 Chrome 中的输出。请注意,插入符号直接跳转到下一行,实际存在的空格已转换为换行符 (NL),并且似乎采用经典的回车加换行 (CR+LF) 形式。因此,在 NL Chrome 看到光标后,就像人眼一样,已经在第 2 行。
第 1 行的最后一个非空格 | wrapping-换行符 | 第 2 行的第一个非空格 |
---|---|---|
偏移量 61 处的“t” | 偏移量 62 处的 NL | 偏移量 63 处的“p” |
现在是 Firefox。插入符号跟随空格,然后跳转到下一行。空间 (SP) 已保留。但是,插入的换行符尚未包含在偏移计算中。此外,它仍然被视为第 1 行的一部分,即人眼看到的光标在第 2 行,但 Firefox 在第 1 行。为什么。
因此,Firefox 在第 1 行的末尾迭代了两次(SP 然后是 NL),但只增加了一次偏移量(对于 SP 和 NL 一起),并且还没有真正移动到第 2 行。所有这些都使这里的事情变得如此混乱。
第 1 行的最后一个非空格 | wrapping-换行符 | 第 2 行的第一个非空格 |
---|---|---|
偏移量 73 处为“n” | SP 和 NL,均位于偏移量 74 处 | 偏移量 75 处的“t” |
治疗
我目前能想到的唯一方法是检测浏览器并引入特定于 Firefox 的解决方法,因此要检查 Firefox,例如使用
const isFirefox = typeof InstallTrigger !== 'undefined';
经过测试,仍然可以使用 Firefox 111。
因此,我们可以通过表示我们是否在Firefox换行符中来弥合这个问题。让我们先添加一些全局变量:
// whether we're in a (Firefox-)NL
let isNewline = false;
// whether we're in Firefox
const isFirefox = typeof InstallTrigger !== 'undefined';
请注意,如有必要,也可以用于其他浏览器。接下来,我们将特定于 Firefox 的跳行添加到处理程序中:isNewline
keyup
/*
* Check whether we're in Firefox and on the edge of a line.
* At need easily extendable for other browsers.
*/
if(isFirefox && rect.y < nextCharacterRect.y)
{
// caret is after the SP, i.e. we're in the NL-sequence
if(isNewline)
{
/*
* Hop straight to the next line by
* de facto enforcing a LF+CR.
*/
domRange = newRange;
domSelection.getRangeAt(0);
rect = domRange.getBoundingClientRect();
// end of Firefox' NL-sequence
isNewline = false;
}
// begin of Firefox' NL-sequence, i.e. we hit the SP
else
isNewline = true;
}
这可以扩展,例如通过选择方向检测进行微调。
让我们在下面的代码片段中将所有内容放在一起。请注意,该 resp. 变成了 而不是 .domRange
rect
let
const
// our denotation values, see above
let isNewline = false;
const isFirefox = typeof InstallTrigger !== 'undefined';
const textEl = document.getElementById("myText")
textEl.addEventListener("keyup", (event) => {
const domSelection = window.getSelection();
if (domSelection && domSelection.isCollapsed && domSelection.anchorNode) {
let offsetNewLine = 0;
let domRange = domSelection.getRangeAt(0);
let 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();
// the line-hopping, see above
if(isFirefox && rect.y < nextCharacterRect.y)
{
if(isNewline)
{
domRange = newRange;
domSelection.getRangeAt(0);
rect = domRange.getBoundingClientRect();
isNewline = false;
}
else
isNewline = true;
}
console.log(`anchorOffset: ${domSelection.anchorOffset}, CURRENT_TOP: ${rect.y}, NEXT_CHAR_TOP: ${nextCharacterRect.y}`);
}
})
.text-container {
width: 500px;
display: inline-block;
border: 1px solid black;
line-height: 20px;
padding: 5px;
}
<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>
结论
可能有一个更优雅、更复杂的解决方案,但目前它可以完成这项工作。从本质上讲,我们通过强制执行一种类似于 Chrome 的换行符来修改 Firefox 的换行行为。唯一剩下的区别是实际换行之前行尾的额外空间,即在Firefox中,我们仍然必须按两次键才能进入下一行,而不是像在Chrome中那样按一次。但这在这里是无关紧要的。否则,两个浏览器的行为现在是等效的。此外,如有必要,此解决方法可以很容易地适用于其他浏览器。LF+CR
确认
使用换行符表示变量的灵感来自@herrstrietzel的一篇文章,其中还讨论了考虑选择方向和鼠标交互的方法。
评论
正如@Krokomot所解释的那样,Firefox有一种古怪的方法来处理换行。
事实上,上一行的末尾和当前行的开头(显示光标/插入符号)都将返回相同的字符索引/位置(或值)。anchorOffset
解决方法可能是在全局变量中保存最后一个字符索引以及最后一个顶部值。y
如果当前字符位置和前一个字符顶部 y 等于前一个字符位置
– 我们取从下一个字符 () 计算出的 y 位置 – 在本例中为换行符之后。anchorOffset + 1
const textEl = document.getElementById("myText");
let bbText = textEl.getBoundingClientRect();
let textElTop = bbText.top;
let textElRight = bbText.right;
let lastCharPos = 0;
let lastTop = 0;
myText.addEventListener("click", (e) => {
updateSelection(e);
});
document.addEventListener("keyup", (e) => {
updateSelection(e);
});
function updateSelection(e) {
let selection = window.getSelection();
let caret = selection.getRangeAt(0);
let range = document.createRange();
let {
anchorNode,
anchorOffset
} = selection;
range.setStart(anchorNode, anchorOffset);
// get y pos of next character
let anchorOffset2 =
anchorOffset < anchorNode.textContent.length - 1 ?
anchorOffset + 1 :
anchorOffset;
let rangeN = document.createRange();
rangeN.setStart(anchorNode, anchorOffset2);
let bb = caret.getBoundingClientRect();
let bb2 = rangeN.getBoundingClientRect();
let height = bb.height;
let top = bb.top - textElTop;
let top2 = bb2.top - textElTop;
// check mouse position on click
let mouseX = e.pageX ? e.pageX : 0;
let distX = mouseX ? Math.abs(bb.left - mouseX) : 0;
let distX2 = mouseX ? Math.abs(bb2.left - mouseX) : 0;
if (
((lastTop && lastTop == top && lastCharPos == anchorOffset) ||
(lastTop && lastTop != top && lastCharPos < anchorOffset)
) ||
(distX > distX2)
) {
top = top2;
}
if (distX < distX2) {
top = bb.top - textElTop;
}
// update
lastCharPos = anchorOffset;
lastTop = top;
mouseX = 0;
// shift line indicator
selectionLine.setAttribute("style", `top:${top}px; height:${height}px;`);
cursor.setAttribute("style", `top:${bb.top}px; left:${bb.left}px;`);
}
body {
font-size: 2em;
margin: 0em;
padding: 11px;
}
* {
box-sizing: border-box;
}
.wrap {
position: relative;
width: 300px;
}
.text-container {
display: block;
border: 1px solid black;
line-height: 1.5em;
padding: 1em;
position: relative;
}
.text-container:focus+.selectionLine {
border-left: 10px solid green;
display: block;
position: absolute;
width: 0;
height: 1em;
top: 0;
right: 0;
}
#cursor {
position: absolute;
width: 0.2em;
height: 0.2em;
top: 0;
right: 0;
background: red;
border-radius: 50%;
}
<div id="info" style="position:absolute; right:0; top:0;"></div>
<div class="wrap">
<div 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.
</div>
<div id="selectionLine" class="selectionLine"></div>
</div>
<div id="cursor"></div>
上面的示例还根据鼠标输入检查新的插入符号位置。
但是,使用向上/向下箭头键时,此方法仍然失败。
红点表示原生插入点位置 - 无法修复。绿色条表示固定的 y 偏移量。
包括选择方向(向前或向后)
我们还在检查关键输入,例如 ,以调整插入符号位置。"ArrowLeft"
"ArrowUp"
相当笨拙,但通过这种方式,我们得到了对箭头键导航的体面支持。
此示例还包括 @Krokomot 建议的 Firefox 用户检测。
const textEl = document.getElementById("myText");
let bbText = textEl.getBoundingClientRect();
let textElTop = bbText.top;
let textElRight = bbText.right;
let lastCharPos = 0;
let lastTop = 0;
let forwards = true;
// simple firefox agent detection
const isFirefox = typeof InstallTrigger !== "undefined";
myText.addEventListener("click", (e) => {
updateSelection(e);
});
document.addEventListener("keyup", (e) => {
updateSelection(e);
});
function updateSelection(e) {
let selection = window.getSelection();
let caret = selection.getRangeAt(0);
let range = document.createRange();
let { anchorNode, anchorOffset } = selection;
range.setStart(anchorNode, anchorOffset);
let bb = caret.getBoundingClientRect();
let height = bb.height;
let top = bb.top - textElTop;
if (isFirefox) {
// get y pos of next character
let anchorOffset2 =
anchorOffset < anchorNode.textContent.length - 1
? anchorOffset + 1
: anchorOffset;
let anchorOffset3 = anchorOffset > 0 ? anchorOffset - 1 : anchorOffset;
let rangeN = document.createRange();
rangeN.setStart(anchorNode, anchorOffset2);
let rangeP = document.createRange();
rangeP.setStart(anchorNode, anchorOffset3);
let bb2 = rangeN.getBoundingClientRect();
let bb0 = rangeP.getBoundingClientRect();
let top2 = bb2.top - textElTop;
let top0 = bb0.top - textElTop;
// check mouse position on click
let mouseX = e.pageX ? e.pageX : 0;
let mouseY = e.pageY ? e.pageY : 0;
// check keybord inputs
let key = e.key ? e.key : "";
let distX = mouseX ? Math.abs(bb.left - mouseX) : 0;
let distX2 = mouseX ? Math.abs(bb2.left - mouseX) : 0;
let distY = mouseY ? Math.abs(bb.top - mouseY) : 0;
let distY2 = mouseY ? Math.abs(bb2.top - mouseY) : 0;
// direction: forward or backward
if (
lastCharPos > anchorOffset ||
key === "ArrowLeft" ||
key === "ArrowUp" ||
(distY && distY < distY2)
) {
forwards = false;
} else if (
lastCharPos < anchorOffset ||
key === "ArrowRight" ||
key === "ArrowDown" ||
(distY && distY > distY2)
) {
forwards = true;
}
// forwards
if (
forwards &&
(lastCharPos == anchorOffset || distX > distX2 || key === "ArrowDown")
) {
top = top2;
}
// backwards
else {
//console.log("back", lastCharPos, anchorOffset);
if (lastCharPos > anchorOffset) {
top = top2;
}
}
// update
lastCharPos = anchorOffset;
lastTop = top;
}
// shift line indicator
selectionLine.setAttribute("style", `top:${top}px; height:${height}px;`);
cursor.setAttribute("style", `top:${bb.top}px; left:${bb.left}px;`);
}
body{
font-size: 2em;
margin:0em;
padding:11px;
}
*{
box-sizing:border-box;
}
.wrap{
position: relative;
width: 300px;
}
.text-container {
display: block;
border: 1px solid black;
line-height: 1.5em;
padding: 1em;
position: relative;
}
.text-container:focus+
.selectionLine {
border-left: 10px solid green;
display:block;
position: absolute;
width:0;
height:1em;
top:0;
right:0;
}
#cursor{
position: absolute;
width:0.2em;
height:0.2em;
top:0;
right:0;
background: red;
border-radius: 50%;
}
<div id="info" style="position:absolute; right:0; top:0;"></div>
<div class="wrap">
<div 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.
</div>
<div id="selectionLine" class="selectionLine"></div>
</div>
<div id="cursor"></div>
评论
console.log()
15.5
35.5
domSelection.anchorNode.textContent[newRangeNextOffset]
+1
newRangeNextOffset
domRange.endOffset