Search code examples
javascriptcssgoogle-chrome-extensioncontenteditable

Prevent Chrome from setting font-size on HTML paste into contenteditable


To reproduce:

  1. Run the MVCE snippet on both Firefox desktop and Chrome desktop.
  2. Open FF desktop, then copy "foobar" from source.
  3. Open Chrome desktop, and paste into target (after the colon in here:)

window.onload = function() {
  const info = document.querySelector('.info'),
    pinfo = document.querySelector('.paste-info'),
    target = document.querySelector('.target');
  setInterval(() => {
    const sel = ".source *, .target *"
    info.innerHTML = '';
    for (const elm of [...document.querySelectorAll(sel)]) {
      info.innerHTML += "TAG: " + elm.tagName + "; TEXT: " + elm.innerText +  "; FONTSIZE: " + window.getComputedStyle(elm)['font-size'] + "<br>";
    }
  }, 1000);
  target.addEventListener('paste', function(e) {
    pinfo.innerHTML += "PASTE HTML: <pre>" + e.clipboardData.getData('text/html').replaceAll('<', '&lt;').replaceAll('>', '&gt;') + '</pre><br>';
  });
};
div[contenteditable] {
  border: 1px solid black;
}
<div class="source" contenteditable=true>Source text: <b>foobar</b></div>

<div style="font-size: 14px">
  <div contenteditable=true class="target">Destination, <h1>paste here:</h1></div>
</div>

<div class="info"></div>
<div class="paste-info"></div>

You will notice that:

  1. Clipboard data contains <b>foobar</b> (see content after PASTE HTML:), but...
  2. The actually pasted HTML has style="font-size: 14px;" set on the b element (The 14px size comes from the parent of the contenteditable).

I expect the pasted HTML to not have any font sizes set on it, because they were not specified in the source clipboard data.

Question: How to force Chrome to not put any font sizes on the pasted HTML, when there is no font-size present on the source HTML?

I tried one workaround: to set font-size: unset/revert on the source, but it causes font-size: unset to also be present in the pasted HTML. I prefer to not have any font-size to be present in the pasted HTML.


The context of this code is a Chrome extension, and I control the text/html data that is pasted into the target. I can attach a paste event listeners on the target contenteditable, but I cannot alter the HTML/styles of contents after it has been pasted.


Solution

  • You can force the "normal" pasting of the HTML markup by using the Selection API.
    The steps are

    • In the paste event handler, get the Range object representing the current selection.
    • Use this Range object to parse the pasted HTML markup into a DocumentFragment object thanks to the createContextualFragment() method.
    • Remove the previously selected content (Range#deleteContents()).
    • Insert the DocumentFragment object we created at step 2, where the cursor is.
    • Collapse the current Range object so that the cursor goes to the end of the newly pasted content.

    Doing all these steps manually will prevent the browser's "smart" handling of the rich-text content; only what's in the clipboard will be parsed.

    window.onload = function() {
      const info = document.querySelector('.info'),
        pinfo = document.querySelector('.paste-info'),
        target = document.querySelector('.target');
      setInterval(() => {
        const sel = ".source *, .target *"
        info.innerHTML = '';
        for (const elm of [...document.querySelectorAll(sel)]) {
          info.innerHTML += "TAG: " + elm.tagName + "; TEXT: " + elm.innerText +  "; FONTSIZE: " + window.getComputedStyle(elm)['font-size'] + "<br>";
        }
      }, 1000);
      target.addEventListener('paste', function(e) {
        pinfo.innerHTML += "PASTE HTML: <pre>" + e.clipboardData.getData('text/html').replaceAll('<', '&lt;').replaceAll('>', '&gt;') + '</pre><br>';
    
        e.preventDefault();
        const markup = e.clipboardData.getData("text/html") ||
          e.clipboardData.getData("text/plain");
        const sel = getSelection();
        const range = sel.getRangeAt(0);
        const frag = range.createContextualFragment(markup);
        range.deleteContents();
        range.insertNode(frag);
        range.collapse();
      });
    };
    div[contenteditable] {
      border: 1px solid black;
    }
    <div class="source" contenteditable=true>Source text: <b>foobar</b></div>
    
    <div style="font-size: 14px">
      <div contenteditable=true class="target">Destination, <h1>paste here:</h1></div>
    </div>
    
    <div class="info"></div>
    <div class="paste-info"></div>

    One big drawback to this method though: This will not make an entry in the edit history. This means that after your users did paste any content there, they'll be unable to undo that action.