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 );