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

How to differentiate first and last position of wrapped text in firefox

提问人:rtom 提问时间:11/13/2023 最后编辑:Rory McCrossanrtom 更新时间:11/20/2023 访问量:458

问:

赏金将在 2 天后到期。这个问题的答案有资格获得 +500 声望赏金。RTOM 希望引起人们对这个问题的更多关注

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

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

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

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

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>

JavaScript HTML CSS

评论

0赞 Rory McCrossan 11/13/2023
请注意,小提琴中的那行使用了损坏的语法。我为您更正了这一点,并将代码移到了内联代码段中,在那里您更有可能获得一些答案,因为人们更容易看到您正在谈论的代码console.log()
0赞 Chris Barr 11/13/2023
我正在使用 Firefox,我在第一行得到一个 for text 的值,在第二行得到一个 for text 的值。据我所知,除非我遗漏了什么,否则它似乎对我有用15.535.5
1赞 rtom 11/13/2023
@ChrisBarr问题出在,特别是如果你在第二行的开头,它会显示你CURRENT_TOP与第一行相同的值。如果您在这些职位上,这对您有用吗?
0赞 Chris Barr 11/14/2023
啊对不起,我没有阅读具体说明。不,在那个位置上,它没有像你所描述的那样报告。
0赞 Chris Barr 11/14/2023
然而,无法想出一个完全可行的解决方案:当我记录下一封信时,它比我预期的要提前。我能够通过在设置值的三元语句中移动来解决这个问题。为什么不直接用于获取光标位置?domSelection.anchorNode.textContent[newRangeNextOffset]+1newRangeNextOffsetdomRange.endOffset

答:

4赞 Krokomot 11/16/2023 #1

首先是诊断,然后是治疗。

诊断

这种奇怪的行为之所以发生,是因为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”

chrome

现在是 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 的解决方法,因此要检查 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 的跳行添加到处理程序中:isNewlinekeyup

/*
* 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. 变成了 而不是 .domRangerectletconst

// 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的一篇文章,其中还讨论了考虑选择方向和鼠标交互的方法。

评论

0赞 Krokomot 11/18/2023
更新了澄清的表述,以强调整个混乱的重点。
0赞 Krokomot 11/19/2023
更新了变通方法代码。评论正在进行中。
0赞 Krokomot 11/19/2023
更新到最终版本。
2赞 herrstrietzel 11/19/2023 #2

正如@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>

评论

0赞 Krokomot 11/19/2023
我喜欢这个想法,唉,至少在 Firefox 111 上,它也显示了与左/右箭头键相同的问题行为——屏幕截图:紧接在 SP+NL 之后,插入符号在第 2 行,红点仍在第 1 行。只有在第 2 行的第一个字符之后,红点才会移动到第 2 行。仅使用键盘进行测试。
0赞 Krokomot 11/19/2023
全局变量是线索,尽管方式与预期不同。必须表示是否在Firefox换行符内。我现在正在评论我的解决方案代码,并将感谢你的灵感。
0赞 Krokomot 11/20/2023
注意:我指的是上面评论中的绿色条,而不是红点。但是,我确认在您当前的代码段中,绿色条现在正确地位于预期的位置。