skip to Main Content

I am working with a contentEditable span where I want to place a position: absolute element on the same line as the cursor. The problem happens when the text gets wrapped because it doesn’t fit – the first and last position of the wrapped lines have weird behaviours.

For both of them, when I am at the first position in the second line the y offset of getBoundingClientRect() is equal to offset of the first line, however if I move one place further on second line the y offset is correctly matching the second line.

In the snippet below this behaviour is displayed for Firefox. For Chrome it seems to work fine although in my full implementation it also has imprecise behavior, but I was able to solve it for chrome. However for Firefox the last position of first line has offset equal to the first line, the first position on second line has offset equal to the first line, afterwards it works fine.

In the example, go to the last place on first line and notice the CURRENT_TOP value in the console says 16. If you go one place right so the cursor is already on next line, it still says 16. if you move one more right, it will say 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>

2

Answers


  1. Diagnosis

    This strange behavior occurs thanks to the fact, that Chrome and Firefox seemingly treat the wrap-newline differently. Execute the following snippet in Chrome and Firefox. The only difference is, that I added

    anchorOffset: ${domSelection.anchorOffset}
    

    to the console output. We’ll discuss the results below.

    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}`);
      }
    })
    .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>

    The browsers wrap at different positions here, but that’s not the point. Look at the output in Chrome first. Note, that the caret jumps directly to the next line, the actually existing space has been transformed to a newline (NL), and seemingly in the classical Carriage Return plus Line Feed (CR+LF) form. So after the NL Chrome sees the cursor already on Line 2.

    last non-whitespace at line 1 wrapping-newline first non-whitespace at line 2
    ‘t’ at offset 61 NL at offset 62 ‘p’ at offset 63

    chrome

    Now Firefox. The caret follows the space and then jumps to the next line. The space (SP) has been preserved. However the inserted newline has not been included into the offset-calculation, and is still treated as part of line 1.

    last non-whitespace at line 1 wrapping-newline first non-whitespace at line 2
    ‘n’ at offset 73 SP and NL, each at offset 74 ‘t’ at offset 75

    firefox

    Approach

    The only way I currently can think of is to detect the browser and introduce a Firefox-specific workaround, so to check on Firefox e.g. with

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

    Tested, and still works, with Firefox 111.

    Login or Signup to reply.
  2. I want to place a position: absolute element on the same line as the cursor.

    I this what you are trying to achieve? In following demo the red arrow follows the caret line.

    const textEl = document.getElementById("myText");
    
    function updatePointer() {
      const domSelection = window.getSelection();
      const topOffset = textEl.getBoundingClientRect().top;
    
      if (domSelection &&
        domSelection.isCollapsed &&
        domSelection.anchorNode) {
    
        const domRange = domSelection.getRangeAt(0);
        const offset = domRange.startOffset;
        if (offset >= 0) {
          const yPos = domRange.getBoundingClientRect().top - topOffset;
          textEl.style.setProperty('--topPos', `${yPos}px`);
        }
      }
    }
    
    textEl.addEventListener("keyup", updatePointer);
    textEl.addEventListener("click", updatePointer);
    .text-container {
      width: 300px;
      display: inline-block;
      border: 1px solid black;
      line-height: 20px;
      padding: 0.4rem;
      position: relative;
      
      --topPos: 0.4rem;
    }
    
    .text-container::after {
      content: "↢";
      color: red;
      font-size: 1.2em;
      font-weight: bold;
      
      position: absolute;
      top: var(--topPos);
      right: -1.2rem;
    }
    <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>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search