Tips for distinguishing the beginning and ending points of wrapped text in the Firefox browser

Within my work, I am utilizing a contentEditable span where I aim to position an element with position: absolute on the same line as the cursor. However, issues arise when text wrapping occurs - causing abnormal behavior at the start and end of wrapped lines.

In both instances, when the cursor is positioned at the start of the second line, the y offset from getBoundingClientRect() matches the offset of the first line. Yet, upon moving one space further into the second line, the y offset aligns correctly with that line.

The code snippet below showcases this behavior specifically in Firefox. While Chrome appears to handle it better, my complete implementation encounters imprecise behavior in Chrome as well, which I managed to resolve. In contrast, Firefox exhibits inconsistent behavior - the last position of the first line displays an offset equal to the first line, while the initial position on the second line mirrors the first line's offset, before functioning properly thereafter.

Explore the example by navigating to the last position on the first line. Notice how the value of CURRENT_TOP displayed in the console remains at 16. Upon shifting right once to move the cursor onto the next line, it still reads 16. Subsequently, moving right once more will display 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>

Answer №1

Begin with the diagnosis, then proceed to the treatment plan.

Diagnosis

This unusual behavior is a result of Chrome and Firefox interpreting newline wrapping differently. To observe this distinction, run the provided code snippet in both Chrome and Firefox. The additional console output includes:

anchorOffset: ${domSelection.anchorOffset}

We will analyze the outcomes below.

The browsers wrap text at varying positions, but the key observation lies in how they handle the newline transition. In Chrome, the cursor shifts immediately to the next line as if a space has been converted into a newline character (NL), akin to a Carriage Return plus Line Feed (CR+LF). Consequently, Chrome places the cursor on Line 2 post new line insertion.

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

Refer to the screenshot illustrating Chrome's behavior. Click here.

In contrast, Firefox moves the caret after the space before transitioning to the next line. While preserving the space, the added newline doesn't factor into the offset calculation. This results in Firefox recognizing the newline as part of Line 1 instead of Line 2.

Last non-whitespace at Line 1 Wrapping-newline First non-whitespace at Line 2
'n' at offset 73 SP and NL, both at offset 74 't' at offset 75

Explore the sample image denoting Firefox's behavior. Access it here.

Treatment Approach

To address this issue, detecting the browser type through some workaround specific to Firefox becomes essential. An example includes:

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

Verified compatible with Firefox version 111.

An effective strategy involves marking whether we are within a Firefox-generated newline. Initially, establish some global variables:

// Tracking presence of Firefox-generated newline
let isNewline = false;
// Identifying Firefox as the current browser
const isFirefox = typeof InstallTrigger !== 'undefined';

Note that 'isNewline' could have broader application beyond Firefox-specific considerations. Subsequently, incorporate line-hopping for Firefox within the 'keyup' handler:

/*
* Ascertaining Firefox presence and proximity to line end.
* Extensible to other browsers as necessary.
*/
if(isFirefox && rect.y < nextCharacterRect.y)
{
    // Position just after space implies within newline sequence
    if(isNewline)
    {
        /*
        * Direct jump to the next line by simulating LF+CR.
        */
        domRange = newRange;
        domSelection.getRangeAt(0);
        rect = domRange.getBoundingClientRect();

        // End of Firefox's newline formation
        isNewline = false;
    }
    // Start of Firefox's newline sequence, marking space location
    else
        isNewline = true;
}

Potential extensions include direction detection for refined adjustments.

Collate all modifications in the ensuing code snippet. Notable changes involve declaring 'domRange' and 'rect' using 'let' instead of 'const'.

Final Thoughts

A more elegant solution might exist, yet the current approach fulfills its purpose. Essentially, adapting Firefox's line-wrapping by enforcing a Chrome-like newline structure ameliorates disparities. Despite minor differences like double keystrokes required in Firefox versus one in Chrome, equivalent behaviors ensue. Furthermore, this workaround easily adapts for addressing issues across various browsers.


Credit

Inspiration stemmed from a suggestion by @herrstrietzel, emphasizing the utility of a newline denotation variable outlined in this post. Considerations for selection direction and mouse interactions were also discussed.

Answer №2

According to @Krokomot, Firefox has a unique way of handling line wraps.

Interestingly, the end of the previous line and the beginning of the current line both return the same character index or anchorOffset value.

A workaround involves storing the last character index and the last top y value in global variables.

If the current character position and the previous top y value are equal to the previous values,

- we use the y position calculated from the next character (anchorOffset + 1) after the line break.

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>

The examples above also address new caret positions based on mouse input.

However, this method still encounters issues with up and down arrow key navigation.

The red spot indicates the native caret position, which cannot be fixed, while the green bar represents the adjusted y offset.

Adding selection direction (forward or backwards)

We're also monitoring key inputs like "ArrowLeft" and "ArrowUp" to adjust the caret position accordingly.

This method may seem cumbersome, but it provides decent support for arrow-key navigation.

This example additionally includes Firefox user detection as advised by @Krokomot.

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>

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Searching for all choices within a select dropdown using Selenium and Python

There's a website called noveltop (.net) that hosts web novels, and on each chapter page, there is a dropdown menu where you can select the chapter you want to jump to. I've been using Selenium with Python and the Firefox (or Chrome) driver to e ...

ways to clear the float on the right side of an image

So I have two images that I am trying to float, like this: img{ float:left; clear:right; } <img src='http://img1.imgtn.bdimg.com/it/u=1005212286,2432746147&fm=21&gp=0.jpg' alt=''><br> <img src ...

Unraveling JSON data retrieved from a MySQL query

After successfully encoding a MySQL result from PHP into JSON, I am now faced with the task of decoding it using JavaScript. Let's assume that my string returned is: [{"0":"x","1":"z"},{"0":"xs","1":"zz"}] I would appreciate some guidance on how to ...

Customize the CSS for a Material UI popover styling

I am currently working with a Material UI popover and attempting to apply CSS styles to it. This is the code for my popover component: import React, { memo, useCallback } from 'react'; import PropTypes from 'prop-types'; import { ...

The interval keeps resetting every time I want the initial one to expire first

I'm currently working on a battle system that utilizes intervals, and I've encountered an issue where it keeps refreshing instead of creating multiple intervals. When I stop pressing the button associated with it, everything goes back to normal. ...

Utilizing Angular2 Observables for Time Interval Tracking

I'm working on a function that needs to be triggered every 500ms. My current approach in angular2 involves using intervals and observables. Here's the code snippet I've implemented so far: counter() { return Observable.create(observer =&g ...

Is there a way to adjust the timepicker value directly using the keyboard input?

Is there a way to control the material-ui-time-picker input using the keyboard instead of clicking on the clock? This is my current code: import React, { Component } from "react"; import { TimePicker } from "material-ui-time-picker"; import { Input as Ti ...

Is it acceptable to use "string" as a variable name in JavaScript?

Any tips on getting the code below to function properly? var x = 'name'; After that, I want to utilize the value inside x like a variable and assign it so that when I call for NAME, I get this outcome: var name = 'NAME'; Can this be ...

Using jQuery to fetch and display content from an external webpage

Is there a more effective method for loading an external web page on the same server? I've experimented with .load() and .get(), but they only load the page after the PHP script is finished. I've also used an iFrame, which displays the informatio ...

Modifying browser.location.href Cancels AJAX Requests

I am facing an issue with my HTML page. I have a button that, when clicked by the user, updates the window.location.href to the URL of a text file. In order to ensure that the file is downloaded and not opened in the browser, the text file is served with C ...

PHP is not receiving any data from the Ajax request

I'm currently attempting to set up my first Ajax example on my MAMP server. Here's how my ajax.html file looks: <html> <head> <script src='ajax.js'></script> </head> <body onload = 'ajax()'> ...

JavaScript date selector allowing the selection of multiple dates

As a beginner in JavaScript, I am struggling to create a datepicker input field that allows multiple selections. Despite researching for solutions, none seem to fit my specific case. Can anyone offer guidance on how to achieve this? Your help is greatly ap ...

Harness the power of Ionic by utilizing the same HTML template across various pages, while easily customizing the content

I need help with streamlining my Ionic app that has multiple pages with similar HTML structures but different content. Is there a way to use just one HTML file and dynamically fill in the content? Should I use a controller for each page, or is there a more ...

Is it not possible to generate HTML tags using jQuery and JavaScript in JSF?

I'm currently working with jsf 2.0 and richfaces 4.0 to develop my application. Occasionally, I incorporate jQuery and JavaScript functions for displaying and hiding elements. However, I've encountered an issue when trying to generate tags within ...

What is the process of linking a PHP file to an HTML form?

Having trouble sending emails with form data since the mailto function isn't working properly. For some reason, the php file is not connecting to the html form. When I try running index.html locally and hit submit, the browser displays php code inste ...

Once appearing, the element quickly vanishes after the fadeIn() function is

In the source code, there is a section that looks like this: <div class="dash page" id="home"> <?php include ('pages/home.php'); ?> </div> <div class="dash page" id="screenshots"> <?php include ('pages/scr ...

Steps for Loading HTML5 Video Element in Ionic 2

Struggling to showcase a list of videos from my server, here's the issue I'm encountering and the layout of the app. https://i.stack.imgur.com/HWVt3.png This is the code snippet in HTML: <ion-list> <button ion-item *ngFor="let v ...

What is the best way to place a div within another div?

I am facing a challenge where I need to insert a new div within another div before all its existing contents. The issue is that the outer divs do not have unique IDs, only classes as selectors. Unfortunately, I do not have control over the generation of th ...

Instructions on transferring input data from a text box to the next page using a session in PHP

Is there a way I can transfer the data from a text box to another page (demo2) and store all the information in my database? I attempted to use sessions, but although the value is being passed to the next page, the text box data isn't... Check out th ...

Is it possible to swap the src of the Next.Js Image Component dynamically and incorporate animations in the

Is there a way to change the src of an image in Nextjs by using state and add animations like fadein and fadeout? I currently have it set up to change the image source when hovering over it. const [logoSrc, setLogoSrc] = useState("/logo.png"); <Image ...