javascripthtmlcsscontenteditable

Strange behavior of non-editable elements inside of contenteditable container in Chrome


I have the markup with non-editbale spans with user-select: none inside contenteditable div. Simplified version of it looks like this:

<div contenteditable="true">
  some random text
  <span contenteditable="false" style="user-select: none">1</span><span contenteditable="false"         style="user-select: none">2</span>
</div>

The key point here is that both spans are on one line. When I put a cursor at the end of line and press Backspace all content is deleted. When putting spans on different lines the code works as expected - Backspace deletes only one last non-editable elements.
Problem occurs in Chrome. FF behaves strange as well, but in another way. I only need to make it work in Chrome for now.

Physically putting spans on different lines in markup solves the problem, but it's no use, because in my case I add non-editable elements to contenteditable dynamically.

const input = document.getElementById("snafu");
const button = document.getElementById("button");

const onClick = () => {
  const tag = document.createElement("span");
  tag.setAttribute("contenteditable", false);
  tag.classList.add("xml-tag");
  tag.innerHTML = "t";
  input.append(tag);
};

button.addEventListener("click", onClick);
body {
  font-family: sans-serif;
}

.input {
  color: green;
  box-shadow: 2px 2px 10px mediumaquamarine;
  padding: 8px;
  border-radius: 8px;
  margin-bottom: 24px;
}

.xml-tag {
  display: inline-flex;
  justify-content: center;
  align-items: center;
  width: 16px;
  height: 16px;
  margin-inline: 2px;
  user-select: none;
  border-radius: 50%;
  background: lime;
  font-size: 10px;
  white-space: pre;
}

.button {
  background: transparent;
  box-shadow: 3px 3px 5px limegreen;
  border: none;
  border-radius: 8px;
  padding: 8px;
}

.button:hover {
  transform: scale(1.1);
  cursor: pointer;
}
<!DOCTYPE html>
<html>

<head>
  <title>Parcel Sandbox</title>
  <meta charset="UTF-8" />
</head>

<body>
  <div id="app">
    <div spellcheck="false" contenteditable="true" class="input" id="snafu">
      <span>Add at least 2 non-editable tags by clicking the button below. Then
          put the cursor at the end of line and press 'Backspace'</span
        >
      </div>

      <button id="button" class="button">Add me a tag, daddy!</button>
    </div>
    <script src="src/index.js"></script>
  </body>
</html>

UPD: When I tried to edit the post code snippet button appeared and I was able to add a snippet eventually.


Solution

  • Listen to keydown events of your textarea. If backspace (or delete) was pressed, get all tags and temporarily remove user-select: none. Then add a setTimeout(func, 0) to restore user-select: none after the last tag was deleted.

    const input = document.getElementById("snafu");
    const button = document.getElementById("button");
    
    const onClick = () => {
      const tag = document.createElement("span");
      tag.setAttribute("contenteditable", false);
      tag.classList.add("xml-tag");
      tag.innerHTML = "t";
      input.append(tag);
    };
    
    button.addEventListener("click", onClick);
    
    // +++++ ADD THIS +++++
    input.addEventListener("keydown", (event) => {
      // if backspace or delete pressed
      if (event.keyCode === 8 || event.keyCode === 46) {
    
        // make tags selectable, so they can be removed
        const tags = input.querySelectorAll(".xml-tag");
        tags.forEach(tag => tag.style["user-select"] = "auto");
    
        // make tags not selectable after deletion
        setTimeout(function() {
          tags.forEach(tag => tag.style["user-select"] = "none");
        }, 0);
      }
    });
    body {
      font-family: sans-serif;
    }
    
    .input {
      color: green;
      box-shadow: 2px 2px 10px mediumaquamarine;
      padding: 8px;
      border-radius: 8px;
      margin-bottom: 24px;
    }
    
    .xml-tag {
      display: inline-flex;
      justify-content: center;
      align-items: center;
      width: 16px;
      height: 16px;
      margin-inline: 2px;
      user-select: none;
      border-radius: 50%;
      background: lime;
      font-size: 10px;
      white-space: pre;
    }
    
    .button {
      background: transparent;
      box-shadow: 3px 3px 5px limegreen;
      border: none;
      border-radius: 8px;
      padding: 8px;
    }
    
    .button:hover {
      transform: scale(1.1);
      cursor: pointer;
    }
    <!DOCTYPE html>
    <html>
      <head>
        <title>Parcel Sandbox</title>
        <meta charset="UTF-8" />
      </head>
      <body>
        <div id="app">
          <div spellcheck="false" contenteditable="true" class="input" id="snafu">
            <span
              >Add at least 2 non-editable tags by clicking the button below. Then
              put the cursor at the end of line and press 'Backspace'</span
            >
          </div>
    
          <button id="button" class="button">Add me a tag, daddy!</button>
        </div>
      </body>
    </html>