Gaurang Tandon
Gaurang Tandon

Reputation: 6753

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.

Upvotes: 4

Views: 633

Answers (3)

Kaiido
Kaiido

Reputation: 136707

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.

Upvotes: 2

Rene van der Lende
Rene van der Lende

Reputation: 5281

(here as anwer because more space than comment, will remove when issue resolved/closed)

Given the system wide behavior of iOS and Windows when copy/pasting text, Chrome seems to provide the generally expected result.

Expected when selecting some text/element and [Ctrl-C] copy:

  • [Ctrl-V] paste with markup
  • [Ctrl-Shift-V] paste without markup

This is exactly what Chrome does, making Firefox the odd one out.

The above obviously doesn't not resolve your issue, but following the above you should be preventing FF from stripping the markup. This will add to your issue, because what did FF do with the original markup??

UPDATE

I think I figured it out: 'foobar' has no font settings, therefore it inherit it's size from nearest parent => 14px. This info is inside the copied 'package' (clipboardData). Firefox strips it by default on [Ctrl-V], where Chrome does not.

As h1 stretches to 100% (filling destination) in FF 'foobar' is 2em while in Chrome it is 14px on [Ctrl-V]. [Ctrl-V] basically should not strip, where [Ctrl-Shift-V] should. Hense my comment that Chrome does it correctly as the font-size after [Ctrl-Shift-V] is indeed 2em because of the h1 stretch.

While redundant in FF, you could strip all info from the clipboardData inside the paste event, or rely on the [Ctrl-V]/[Ctrl-Shift-V] mechanism.

UPDATE 2

There is more going on. I created a codepen for testing.

Pasting in succession right after 'paste here:' and 'Destination, ' with both [Ctrl-V] and [Ctrl-Shift-V] randomly shows that info gets stripped when [Ctrl-Shift-V] is used first a few times. After using [Ctrl-V] and then [Ctrl-Shift-V] again, the stripping take no longer place (the text stays bold in either case).

Leads me to think that next to the different [Ctrl-V]/[Ctrl-Shift-V] handling of both browsers, the internal text editor has a few flaws I cannot get my head around...

Upvotes: 0

John
John

Reputation: 5335

Maybe its possible to intercept the pasting event, and alter the contents of the paste to force it to paste as plain text using js.

I've added an id of id="editor" on the contenteditable div. and added the following js code:

const editorEle = document.getElementById('editor');

// Handle the paste event
editorEle.addEventListener('paste', function (e) {
    // Prevent the default action
    e.preventDefault();

    // Get the copied text from the clipboard
    const text = e.clipboardData
        ? (e.originalEvent || e).clipboardData.getData('text/plain')
        : // For IE
        window.clipboardData
        ? window.clipboardData.getData('Text')
        : '';

    if (document.queryCommandSupported('insertText')) {
        document.execCommand('insertText', false, text);
    } else {
        // Insert text at the current position of caret
        const range = document.getSelection().getRangeAt(0);
        range.deleteContents();

        const textNode = document.createTextNode(text);
        range.insertNode(textNode);
        range.selectNodeContents(textNode);
        range.collapse(false);

        const selection = window.getSelection();
        selection.removeAllRanges();
        selection.addRange(range);
    }
});

here's a snippet of it running. let me know if this solved your issue:

const editorEle = document.getElementById('editor');

// Handle the `paste` event
editorEle.addEventListener('paste', function (e) {
    // Prevent the default action
    e.preventDefault();

    // Get the copied text from the clipboard
    const text = e.clipboardData
        ? (e.originalEvent || e).clipboardData.getData('text/plain')
        : // For IE
        window.clipboardData
        ? window.clipboardData.getData('Text')
        : '';

    if (document.queryCommandSupported('insertText')) {
        document.execCommand('insertText', false, text);
    } else {
        // Insert text at the current position of caret
        const range = document.getSelection().getRangeAt(0);
        range.deleteContents();

        const textNode = document.createTextNode(text);
        range.insertNode(textNode);
        range.selectNodeContents(textNode);
        range.collapse(false);

        const selection = window.getSelection();
        selection.removeAllRanges();
        selection.addRange(range);
    }
});




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 id="editor" class="target">Destination, <h1>paste here:</h1></div>
</div>

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

Upvotes: 0

Related Questions