42

Is there a way in javascript to copy an html string (ie <b>xx<b>) into the clipboard as text/html, so that it can then be pasted into for example a gmail message with the formatting (ie, xx in bold)

There exists solutions to copy to the clipboard as text (text/plain) for example https://stackoverflow.com/a/30810322/460084 but not as text/html

I need a non flash, non jquery solution that will work at least on IE11 FF42 and Chrome.

Ideally I would like to store both text and html versions of the string in the clipboard so that the right one can be pasted depending if the target supports html or not.

Community
  • 1
  • 1
kofifus
  • 11,635
  • 8
  • 73
  • 114
  • 1
    Possible duplicate of [javascript copy rich text contents to clipboard](http://stackoverflow.com/questions/23934656/javascript-copy-rich-text-contents-to-clipboard) – Alexander O'Mara Dec 10 '15 at 00:57
  • Are you sure, it looks like it's asking how to copy a section of HTML as rich-text, which I think is what you are looking for. The answers leave much to be desired though. – Alexander O'Mara Dec 10 '15 at 01:03
  • An answer from another question that might help: http://stackoverflow.com/a/31945909/3155639 – Alexander O'Mara Dec 10 '15 at 01:05
  • If you use clipboard.js (or whatever) you can then [use `insertAdjacentHTML` to add the text _as_ HTML](http://jsfiddle.net/ew8oLzhe/). – Andy Dec 10 '15 at 01:07
  • Alexandra, While that answer is relevant, the approved answer there points to http://stackoverflow.com/questions/8743559/copying-an-image-to-clipboard-using-javascript-jquery marked as a duplicate of http://stackoverflow.com/questions/2321606/copy-image-to-clipboard-from-browser-in-javascript who's answer says it's impossible for security reasons. That is incorrect as you can definitely copy text as is shown here http://stackoverflow.com/a/30810322/460084 and I am confident extending that solution to html is feasible (by using execCommand('copy') over a div), but I couldn't get it to work. – kofifus Dec 10 '15 at 01:10
  • Andy I don't understand, I run your jsfiddle but nothing gets copied to the clipboard – kofifus Dec 10 '15 at 01:13
  • 1
    Nicely done. And since I like some structured code, I refactored your work into a small JavaScript class. See EDIT #3 in my answer. – Loilo Dec 11 '15 at 04:01
  • Possible duplicate of [How do I copy to the clipboard in JavaScript?](https://stackoverflow.com/questions/400212/how-do-i-copy-to-the-clipboard-in-javascript) – Liam Jul 13 '18 at 12:41

3 Answers3

46

Since this answer has gotten some attention, I have completely rewritten the messy original to be easier to grasp. If you want to look at the pre-revisioned version, you can find it here.


The boiled down question:

Can I use JavaScript to copy the formatted output of some HTML code to the users clipboard?


Answer:

Yes, with some limitations, you can.


Solution:

Below is a function that will do exactly that. I tested it with your required browsers, it works in all of them. However, IE 11 will ask for confirmation on that action.

Explanation how this works can be found below, you may interactively test the function out in this jsFiddle.

// This function expects an HTML string and copies it as rich text.

function copyFormatted (html) {
  // Create container for the HTML
  // [1]
  var container = document.createElement('div')
  container.innerHTML = html

  // Hide element
  // [2]
  container.style.position = 'fixed'
  container.style.pointerEvents = 'none'
  container.style.opacity = 0

  // Detect all style sheets of the page
  var activeSheets = Array.prototype.slice.call(document.styleSheets)
    .filter(function (sheet) {
      return !sheet.disabled
    })

  // Mount the container to the DOM to make `contentWindow` available
  // [3]
  document.body.appendChild(container)

  // Copy to clipboard
  // [4]
  window.getSelection().removeAllRanges()

  var range = document.createRange()
  range.selectNode(container)
  window.getSelection().addRange(range)

  // [5.1]
  document.execCommand('copy')

  // [5.2]
  for (var i = 0; i < activeSheets.length; i++) activeSheets[i].disabled = true

  // [5.3]
  document.execCommand('copy')

  // [5.4]
  for (var i = 0; i < activeSheets.length; i++) activeSheets[i].disabled = false

  // Remove the container
  // [6]
  document.body.removeChild(container)
}

Explanation:

Look into the comments in the code above to see where you currently are in the following process:

  1. We create a container to put our HTML code into.
  2. We style the container to be hidden and detect the page's active stylesheets. The reason will be explained shortly.
  3. We put the container into the page's DOM.
  4. We remove possibly existing selections and select the contents of our container.
  5. We do the copying itself. This is actually a multi-step process: Chrome will copy text as it sees it, with applied CSS styles, while other browsers will copy it with the browser's default styles. Therefore we will disable all user styles before copying to get the most consistent result possible.

    1. Before we do this, we prematurely execute the copy command. This is a hack for IE11: In this browser, the copying must be manually confirmed once. Until the user clicked the "Confirm" button, IE users would see the page without any styles. To avoid this, we copy first, wait for confirmation, then disable the styles and copy again. That time we won't get a confirmation dialog since IE remembers our last choice.
    2. We actually disable the page's styles.
    3. Now we execute the copy command again.
    4. We re-enable the stylesheets.
  6. We remove the container from the page's DOM.

And we're done.


Caveats:

  • The formatted content will not be perfectly consistent across browsers.

    As explained above, Chrome (i.e. the Blink engine) will use a different strategy than Firefox and IE: Chrome will copy the contents with their CSS styling, but omitting any styles that are not defined.

    Firefox and IE on the other hand won't apply page-specific CSS, they will apply the browser's default styles. This also means they will have some weird styles applied to them, e.g. the default font (which is usually Times New Roman).

  • For security reasons, browsers will only allow the function to execute as an effect of a user interaction (e.g. a click, keypress etc.)

Loilo
  • 9,930
  • 5
  • 32
  • 41
  • this looks fantastic Loilo! if you can work this into a function that accepts an html string (ie 'xxboldyy') and puts it on the clipboard as rich text I'll mark it as the answer. Thanks ! – kofifus Dec 10 '15 at 01:27
  • thx! marked it as the answer. Do you think there's a way to add both text and html to the clipboard at the same time so that the correct version will be chosed depending on the target (ie pasting into notepad vs pasting into gmail) ?? – kofifus Dec 10 '15 at 02:15
  • 1
    You can only copy one thing to the clipboard, always. Notepad just strips the styled parts away. This will happen to your copied HTML in exactly the same way (try to paste the copied stuff from the JSFiddle to notepad). – Loilo Dec 10 '15 at 02:18
  • There are still problems with the code. I added a textarea and copy on ctrl+C but FF fails and consoles show 'too much recursion' ! see http://jsfiddle.net/d740eo04/7/ just hit ctrl+c in the textarea – kofifus Dec 10 '15 at 02:36
  • Seems you're violating a security feature with that. Kind of understandable, you want to hack into the copy event and then actually copy something else, that seems like a risk to me, too. You have to a) not use the copy event but rather listen for keydown and b) take focus off the textarea before copying. http://jsfiddle.net/Loilo/d740eo04/10/ – Loilo Dec 10 '15 at 12:36
  • If you want to do it like in the example above: Remember to distinguish between OS X and Windows (different shortcuts) and if you don't want to confuse the user you should give the focus back to the textarea after copying, maybe even restore the exact selection. – Loilo Dec 10 '15 at 12:37
  • This is great, thank you so much for your html2clipboard() it sorted my problem. I was getting an error of `Discontiguous selection is not supported`. To fix this i added the following to the first line of the function `window.getSelection().removeAllRanges();` which removes all exisiting ranges before adding the new one – Mr Giggles Jan 17 '17 at 22:53
  • You're welcome. I'll look into your issue and probably update the repo and the answer anyway since this was one of my first SO answers (and GitHub repos) and I've learned a lot since back then. – Loilo Jan 18 '17 at 07:46
  • Your jsFiddle saved my life and made me believe that there might indeed be a god in the world. Thank you. – samuraiseoul Feb 10 '17 at 23:15
  • Thanks, works for me. I may end up modifying this to just pass data into a table in another browser tab, rather than copying to clipboard. In any case, thanks for the foundation! – MarsAndBack Jan 05 '18 at 22:56
14

There is a much simpler solution. Copy a section of your page (element) than copying HTML.

With this simple function you can copy whatever you want (text, images, tables, etc.) on your page or the whole document to the clipboard. The function receives the element id or the element itself.

function copyElementToClipboard(element) {
  window.getSelection().removeAllRanges();
  let range = document.createRange();
  range.selectNode(typeof element === 'string' ? document.getElementById(element) : element);
  window.getSelection().addRange(range);
  document.execCommand('copy');
  window.getSelection().removeAllRanges();
}

How to use:

copyElementToClipboard(document.body);
copyElementToClipboard('myImageId');
Kate Orlova
  • 2,793
  • 5
  • 9
  • 28
Theo
  • 241
  • 3
  • 3
  • Could you please clarify, is it possible to copy few elements (two sections for example), in loop maybe? Thanks. – Schroet Feb 25 '20 at 18:17
  • This solution works well (Chrome & Firefox on MacOS). Note it turns out when I put button making call to get selection of the div with 3 links right before the button and I copied from Chrome to gmail a square box (the button?) showed up. Adding   just before – Raymond Naseef Apr 20 '20 at 21:56
  • @Schroet - I checked doing this in these ways without success: [A] one range with selectNode for each element, [B] call selection.addRange() once for each range, [C] call selection.addRange.apply(, ranges). Not sure why selection looks like it can add multiple ranges... – Raymond Naseef Apr 20 '20 at 21:56
  • This worked excellently. Tested in Chrome and even IE11! Thanks! – Dylan Watson Jul 02 '20 at 02:50
8

I have done a few modifications on Loilo's answer above:

  • setting (and later restoring) the focus to the hidden div prevents FF going into endless recursion when copying from a textarea

  • setting the range to the inner children of the div prevents chrome inserting an extra <br> in the beginning

  • removeAllRanges on getSelection() prevents appending to existing selection (possibly not needed)

  • try/catch around execCommand

  • hiding the copy div better

On OSX this will not work. Safari does not support execCommand and chrome OSX has a known bug https://bugs.chromium.org/p/chromium/issues/detail?id=552975

code:

clipboardDiv = document.createElement('div');
clipboardDiv.style.fontSize = '12pt'; // Prevent zooming on iOS
// Reset box model
clipboardDiv.style.border = '0';
clipboardDiv.style.padding = '0';
clipboardDiv.style.margin = '0';
// Move element out of screen 
clipboardDiv.style.position = 'fixed';
clipboardDiv.style['right'] = '-9999px';
clipboardDiv.style.top = (window.pageYOffset || document.documentElement.scrollTop) + 'px';
// more hiding
clipboardDiv.setAttribute('readonly', '');
clipboardDiv.style.opacity = 0;
clipboardDiv.style.pointerEvents = 'none';
clipboardDiv.style.zIndex = -1;
clipboardDiv.setAttribute('tabindex', '0'); // so it can be focused
clipboardDiv.innerHTML = '';
document.body.appendChild(clipboardDiv);

function copyHtmlToClipboard(html) {
  clipboardDiv.innerHTML=html;

  var focused=document.activeElement;
  clipboardDiv.focus();

  window.getSelection().removeAllRanges();  
  var range = document.createRange(); 
  range.setStartBefore(clipboardDiv.firstChild);
  range.setEndAfter(clipboardDiv.lastChild);
  window.getSelection().addRange(range);  

  var ok=false;
  try {
     if (document.execCommand('copy')) ok=true; else utils.log('execCommand returned false !');
  } catch (err) {
     utils.log('execCommand failed ! exception '+err);
  }

  focused.focus();
}

see jsfiddle where you can enter html segment into the textarea and copy to the clipboard with ctrl+c.

kofifus
  • 11,635
  • 8
  • 73
  • 114
  • 1
    You don't want to set tabindex to 0 or it will be on the tab index i.e. it will be one of the items that gets selected when the user presses the tab key. To make it focusable, but not accessible by the user pressing tab, set the tabindex to -1. I suggested this as an edit, but it was rejected. – Jools Jun 04 '20 at 09:37