css 如何区分Firefox中 Package 文本的首尾位置

wecizke3  于 11个月前  发布在  其他
关注(0)|答案(2)|浏览(88)

bounty将在3天后过期。回答此问题可获得+500声望奖励。rtom希望引起更多关注此问题。

我正在处理一个contentEditable span,我想把一个position: absolute元素放在光标所在的同一行上。当文本被换行时,问题就发生了,因为它不合适-换行的第一个和最后一个位置有奇怪的行为。
对于这两个,当我在第二行的第一个位置时,getBoundingClientRect()y偏移量等于第一行的偏移量,但是如果我在第二行上再移动一个位置,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>
avwztpqn

avwztpqn1#

诊断

这种奇怪的行为发生的原因是,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;

    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(`anchorOffset: ${domSelection.anchorOffset}, CURRENT_TOP: ${rect.y}, NEXT_CHAR_TOP: ${nextCharacterRect.y}`);
  }
})

x

.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中的输出。注意,caret直接跳到下一行,实际存在的空格已经 * 转换 * 为换行符(NL),并且似乎是经典的回车加换行符(CR+LF)形式。因此,NL Chrome看到光标后,就像人眼一样,已经在2号线了。
| 第1行的最后一个非空格|换行|第2行的第一个非空格|
| --|--|--|
| 偏移量61处的“t”|偏移62处的NL|偏移量63处的“p”|
x1c 0d1x的数据
现在Firefox。插入符号跟随空格,然后**跳到下一行。空格(SP)被保留。但是 * 插入的 * 换行符 * 没有 * 被包括在偏移计算中。此外,它仍然被视为第1行的一部分,即人眼看到的光标在第2行,但Firefox在第1行。无论如何。
所以Firefox在第1行的末尾迭代了 * 两次 *(SP然后NL),但是只增加了 * 一次 * 偏移量(SP和NL一起),而且还没有真正移动到第2行。所有这些都使得这里的事情变得如此混乱。
| 第1行的最后一个非空格|换行|第2行的第一个非空格|
| --|--|--|
| 偏移量73处的“n”|SP * 和 * NL,偏移量均为74|偏移量为75的“t”|


方法

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

const isFirefox = typeof InstallTrigger !== 'undefined';


经过测试,仍然适用于Firefox 111。

9bfwbjaz

9bfwbjaz2#

正如@Krokomot所解释的那样,Firefox有一种古怪的方式来处理换行。
事实上,前一行的末尾 * 和当前行的开头 *(显示光标/插入符号)将返回相同的字符索引/位置(或anchorOffset值)。
解决方法是保存全局变量中最后一个字符索引和最后一个顶部y值。
如果当前字符位置和前一个top y等于前一个

  • 我们从下一个字符(anchorOffset + 1)计算y位置-在这种情况下,在换行符之后。
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>

上面的示例还根据鼠标输入检查新插入符号的位置。
然而,当使用向上/向下箭头键时,这种方法仍然失败。

相关问题