7

I've just noticed that div contenteditable in reports 1 newline as 2 newlines in Firefox.

Is this a bug or am I missing something?

In the following example just type:

Hello

World

in the contenteditable.

Grabbing the value using innerText reports it as:

Hello



World

const textarea = document.querySelector('#textarea')

textarea.addEventListener('keydown', e => {
  console.log(textarea.innerText)
})
#textarea {
  width: 200px;
  height: 75px;
  outline: 1px solid black;
}
  <div id="textarea" contenteditable="true"></div>
nicholaswmin
  • 21,686
  • 15
  • 91
  • 167

2 Answers2

3

Update of 2020-08-27

I think the answer to this may be not to use innerText, but instead textContent, in combination with the CSS directive white-space: pre-wrap;

See this answer.


As I say in my comment, as of today, 2020-07-26, this execCommand solution seems to have no effect.

Looking at the problem I found that if I have this in an editable DIV:

asdfkadsfaklshdfkajsa

sdfaalkj 

Pinkerton detective agency

sdfaldskfhalks

... and I add a newline at the end of the first line there, in Firefox I get the following innerHTML:

<div>asdfkadsfaklshdfkajsa</div><div><br></div><br>sdfaalkj <br><br>Pinkerton detective agency<br><br>sdfaldskfhalks<br>

... and counting the newlines (\n) on innerText there are said to be 9 newlines. In Chrome (where this problem does not occur) I get the following innerHTML:

asdfkadsfaklshdfkajsa<div><br><br>sdfaalkj <br><br>Pinkerton detective agency<br><br>sdfaldskfhalks<br></div>

and there are said to be 8 newlines in the innerText. We can see that the use of the DIV tag is utterly and completely, indeed absurdly different. So much for Firefox's avowed aim of bringing this handling into line with the other browsers!

A possible solution
The solution is to interfere with the generated HTML, and will of course not apply where you deliberately have DIV elements inside your text. But for processing the plainest of plain old text, where DIV elements really have no place anyway, this solution seems to work (this code is to be applied only in Firefox, of course):

if( comp.innerHTML.toLowerCase().includes( '<div>' )){
        // spurious double new BR happens under certain circs
        if( comp.innerHTML.includes( '<div><br><\/div><div><br><\/div>' )){
            comp.innerHTML = comp.innerHTML.replace(/<div><br><\/div><div><br><\/div>/i, '<br>' );
            // see below: must be decremented 
            startOffset -= 1;
        }
        // simple new BR
        comp.innerHTML = comp.innerHTML.replace(/<div><br><\/div>/i, '<br>' );
        // console.log( comp.innerHTML )
        comp.innerHTML = comp.innerHTML.replace(/<\/div>/gi, '<br>' );
        // console.log( comp.innerHTML )
        comp.innerHTML = comp.innerHTML.replace(/<div>/gi, '' );
        console.log( comp.innerHTML )
}

I got the above wrong in my first attempt. My reason for including the console.log each time is so you can see what's happening. In fact you HAVE to substitute <BR> for </DIV> before you remove the <DIV>s: if you remove the <DIV>s, this also automatically removes the closing </DIV> tags. But if you only remove the closing tags the opening tags remain!

... after that, in FF, you have an innerText with 8 newlines and things are a bit more like what you'd hope/expect.

Unfortunately it turns out that things aren't quite this simple: repeated pressing of the Enter key sometimes results in two <DIV><BR></DIV>s being inserted where only one should be. However, if you physically click somewhere else and then return the caret to the DIV it works OK next time you click Enter.

For detection of the browser, see here.

You also have another problem: setting the innerHTML puts the cursor back to the start of the content. This is not trivial: bear in mind that, as a DOM structure, what you now have is several text nodes interspersed with <BR> nodes. See proposed solution below, which appears to work.

Also be aware that, if you have a mutation observer listening for changes in the document, inserting a new line like this triggers a vast number of asynchronous mutation events. In fact the number of events appears to be related to the number of BR nodes in your DIV. Even if the mutation observer is disconnected as a response to the first such event you will still get these. In my case the above code was triggered each time, but it's harmless as, once you have processed as above, subsequent calls will not change it any more (i.e. all DIVs have been removed from the innerHTML).

Appropriate solution for caret get/set
As I said, setting innerHTML sets the caret to the beginning of the text. Not good. We also know (or assume) that this text contains new lines (using <BR> in the HTML) but that it is not otherwise marked up, something we can take advantage of: the childNodes of the DIV should be just #text nodes and BR nodes.

First experiments seem to show that we can use these functions. First, to get the caret position before we apply our changes, counting each BR as 1 character:

function calculateTotalOffset(node, offset) {
    let total = offset;
    let currNode = node;
    while (currNode !== comp ) {
        if( currNode.tagName === 'BR'){
            total += 1;
        }
        if(currNode.previousSibling) {
            total += currNode.previousSibling.textContent.length;
            currNode = currNode.previousSibling;
        } else {
            currNode = currNode.parentElement;
        }
    }
    return total;
}
const sel = window.getSelection();
const range = sel.getRangeAt( 0 );
let startOffset = calculateTotalOffset(range.startContainer, range.startOffset);

... to set our caret once we have changed our innerHTML:

function applyTotalOffset( headNode, offset ){
    let remainingOffset = offset;
    for( let node of headNode.childNodes ){
        if( node.nodeName === '#text' ){
            if( node.length > remainingOffset ){
                range.setStart( node, remainingOffset );
                return;
            }
            else {
                remainingOffset -= node.length;
            }
        }
        else if( node.nodeName === 'BR' ){
            if( remainingOffset === 0 ){
                range.setStart( node, 0 );
                return;
            }
            else {
                remainingOffset -= 1;
            }
        }
        else {
            throw new Error( 'not plain text: ' + headNode.innerHTML );
        }
    }
}
// add 1 to returned offset because the cursor should advance by 1, after adding new line
applyTotalOffset( comp, startOffset + 1 );
mike rodent
  • 14,126
  • 11
  • 103
  • 157
2

This appears to be a bug with Firefox's implementation of innerText.

New lines in contenteditable historically had a lack of consensus on the precise implementation. Gecko inserted <br>s, IE and Opera's Presto wrapped the line in <p> tags, and Webkit and Blink wrapped the line in <div> tags. With its old implementation that used <br> tags, Firefox's innerText worked just fine, exactly how your question wants it to.

However today's modern browsers have reached a general consensus for the sake of consistency. Specifically for Firefox, MDN stated:

As of Firefox 60, Firefox will be updated to wrap the separate lines in <div> elements, matching the behavior of Chrome, modern Opera, Edge, and Safari.

When modern browsers insert an empty new line, they do so by wrapping a <br> in <div> tags. It seems that Firefox's innerText implementation is interpreting this as two new lines.


A workaround is quite simple. The execCommand method (unofficial W3 draft but supported by Firefox which is all we need) allows you to manipulate a contenteditable's content with defined commands. One such command is defaultParagraphSeparator, which allows you to specify whether to use <div> or <p> as the container for lines in your contenteditable.

Firefox - and only Firefox - has additionally implemented support for <br>s with defaultParagraphSeparator. With this command the contenteditable behaves like it would have prior to FF60, that is, inserting line breaks instead of wrapping lines within a container.

Thus, all you need to do is put:

document.execCommand("defaultParagraphSeparator", false, "br");

in your JavaScript. All versions of Firefox will then be using <br>s instead of containers for new lines in contenteditable, thus making innerText correctly interpret new lines. And every browser other than Firefox will ignore it.

jla
  • 4,191
  • 3
  • 27
  • 44
  • 2
    As of today (FF 72) this command has no effect. DIVs still being used, 2 newlines are being added rather than one. – mike rodent Jul 26 '20 at 11:22