Search code examples
javascripthtmlnewlinecontenteditableinnertext

Div innerText loses new lines after setting display to none


While programming a custom WYSIWYG HTML based editor I came to this strange behavior.

jsfiddle

One enters some text containing new lines in a div, which has contentEditable set to true.

At that moment div.innerText contains exactly the entered text including the new lines.

Then one sets div.style.display = none and re-checks the div.innerText: it is the same text, but the new lines are removed.

Why is that? Is there "standard behavior" for this case?

(Tested in both FF Developer Edition 89.0b3 (64-bit) and Chrome Version 90.0.4430.85 (Official Build) (64-bit))

=> Follow up There is also another similar strange problem:

var d = document.getElementById("divTest")

function setup() {
    d.innerText = "1\n2"
}

function log() {
    console.log(d.innerText)
  logChildren()
}

function logChildren() {
    console.log("Child nodes: ");
    d.childNodes.forEach(node => {
    console.log(node.toString());
  });
}
div[contenteditable] {
  border: 1px solid red;
}
<div contenteditable="true" id="divTest"></div>
<input type="button" value="setContent" onclick="setup()"/>
<input type="button" value="log" onclick="log()"/>

Click on the setContent button and then on log button. The output is as expected:

1
2

[object Text]
[object HTMLBRElement]
[object Text]

Then click inside the input div. Press enter after the 2 to go to a new line, and press 3. One gets

1
2
3

in the div and one would expect to get the same in the div.innerText, but it is unfortunately:

1

2
3
Child nodes: 
[object Text]
[object HTMLBRElement]
[object HTMLDivElement]
[object HTMLDivElement]

Why would 1 be a [object Text] but 2 and 3 [object HTMLDivElement] ? Why would there be empty line between 1 and 2? etc. ...

It does not make any sense to me.


Solution

  • The implementation of innerText depends on whether or not the element is visible. As @Nisala noted in the comments, if you set the display attribute of the div back to block, the innerText contains your newline characters again.

    const input = document.querySelector("#input");
    
    function performExperiment() {
      console.log(`innerText before style change: ${input.innerText}`);
      
      input.style.display = "none";
      console.log(`innerText after first style change: ${input.innerText}`);
      
      input.style.display = "block";
      console.log(`innerText after second style change: ${input.innerText}`);
    }
    #input {
      border: 1px solid black;
    }
    <p>Enter multiple lines of text into the box below, then click the button</p>
    <div id="input" contenteditable="true"></div>
    <button onclick="performExperiment()">Experiment</button>


    If we have a look at the innerText documentation, we see the first step of the behavior for the getter is defined as follows:

    1. If this is not being rendered or if the user agent is a non-CSS user agent, then return this's descendant text content.

    Note: This step can produce suprising results, as when the innerText getter is invoked on an element not being rendered, its text contents are returned, but when accessed on an element that is being rendered, all of its children that are not being rendered have their text contents ignored.

    So when our div is not being rendered, we should expect that innerText returns the textContent of our div. Indeed, that is what we see.

    const input = document.querySelector("#input");
    
    function performExperiment() {
      input.style.display = "none";
      console.log(`innerText: ${input.innerText}`);
      console.log(`textContent: ${input.textContent}`);
    }
    #input {
      border: 1px solid black;
    }
    <p>Enter multiple lines of text into the box below, then click the button</p>
    <div id="input" contenteditable="true"></div>
    <button onclick="performExperiment()">Experiment</button>


    So why are the newlines present in our innerText when the div is visible? The documentation continues:

    1. Let results be a new empty list

    2. For each child node node of this:

    1. Let current be the list resulting in running the inner text collection steps with node. Each item in results will either be a string or a positive integer (a required line break count).

    In this case, innerText is ignoring textContent and is instead operating on the childNodes list. Let's see what the value of that is for our div:

    const input = document.querySelector("#input");
    
    function performExperiment() {
      input.childNodes.forEach(node => {
        console.log(node.toString());
      });
    }
    #input {
      border: 1px solid black;
    }
    <p>Enter multiple lines of text into the box below, then click the button</p>
    <div id="input" contenteditable="true"></div>
    <button onclick="performExperiment()">Experiment</button>


    As you can see, pressing the ENTER key adds a newline to the content of our div by adding a div to the childNodes list of our div. Why this is the case is outside the scope of this question, but would make for a good question on its own.

    If you're working on an in-page editor, the HTML spec has a section containing best practices.


    To recap:

    If the div is visible, the innerText getter uses the textContent property of the div.

    If the div is not visible, the inner text collection steps are followed for each node in the childNodes tree and the results are concatenated together.

    When computing the value of innerText for our div, the value of the display attribute matters because it determines whether the textContent property or the evaluation of the childNodes tree will be used.


    Note: There's a little more information in this answer by @Domino.