Customize Clipboard Content on Copy: Caveats

Dec 14, 2023

A while back I needed a way to edit the text that ended up in the user's clipboard when they selected a portion of the website and copied it. First of all, let me tell you that you SHOULD NOT do this without a good reason. It's so easy to annoy users by allowing this to become just another way to do "right-click-disable" and that's ALWAYS a no-no.

In my case, if I did things right, the user would not even notice I did anything to their clipboard; in fact if I left things alone is when things would appear broken! The setup was this:

In headings of the page, product model numbers are listed in the largest font. Sometimes a list of product model numbers wraps to a second or third line, and this is fine, but I would MUCH prefer if the browser broke lines at the comma-space and not the hyphens within model numbers. Examples:

Bad:

P-20-GG-3000, P-20-GG-3001, P-20-GG-3002, P-20-GG-3003, P-20-GG-3004

For the above, you may need to adjust the width of the page to see that the browser breaks the model number list both at comma-spaces and hyphens within model numbers.

Good:

P‑20‑GG‑3000, P‑20‑GG‑3001, P‑20‑GG‑3002, P‑20‑GG‑3003, P‑20‑GG‑3004

So, how'd I do that? The fix is replacing all of the regular hyphens in the model numbers with non-breaking-hyphens: U+2011. However, while this makes things visually appealing, it comes with several downsides:

  • Searching for "P[hyphen]20[hyphen]GG[hyphen]3000" won't match the version using the non-breaking hyphen.
  • A user copying the model number from the page will get the non-breaking-hyphens in their clipboard instead of regular hyphens. This can be a problem if, for example, they paste it somewhere, then later try to search for what they pasted using regular hyphens and can't find it.

The solution to the first problem is easy. Just write the model numbers in the HTML using normal hyphens, then have a script run on page-load that changes all hyphens in just the top header to non-breaking hyphens. This will break find-in-page of course, but search engines will get the regular hyphens and that's the important bit.

It's the second problem where things get tricky. There's lots of browser security surrounding a user's clipboard contents; you can't just open it up and edit it directly, but you can paste new content in there. So if you know what the user was trying to copy, you can capture the copy event and fill the clipboard manually. There are actually quite a few helpful answers to this problem using this method on the various technical sites, but I found they all have a significant flaw: They don't properly mimic the contents of an uncaptured copy event when it comes to images.

Default Behaviour

Try copying this sentence, including the inline image, and pasting it into the te[this is the alt text of the inline image]tarea below.

There's no event capturing going on here; this is your browser's default behaviour. You can see that images within your highlighted selection have their alt-text copied. This is a pretty neat use case for alt-text! So now lets see what happens when we capture the copy event and try to fill the clipboard ourselves.

Capturing the Copy Event

Try copying this sentence, including the inline image, and pasting it into the te[this is the alt text of the inline image]tarea below.

We're using the Javascript code as suggested in this StackOverflow answer:

document.addEventListener('copy', function(e) {
  let text = window.getSelection().toString();

  /* Modify the text as you like here */

  e.clipboardData.setData('text/plain', text);
  e.preventDefault();
}, false);

Immediately you'll notice that our image alt-text has disappeared. It seems that the toString() method ignores images and their alt-text when giving us the selected text. So while we've gained the ability to modify the text placed in the user's clipboard, we've lost the alt-text content of any images they included in the selection. Is it possible to work around this?

Cloning the Selection Range

Well, the only way we're going to put that alt-text back is if we do it ourselves, and to do that, we'll need to clone the section of document that the user has highlighted.

Try copying this sentence, including the inline image, and pasting it into the te[this is the alt text of the inline image]tarea below.

We're using the Javascript code modified from this StackOverflow answer:

document.addEventListener('copy', function(e) {
  let contents = window.getSelection().getRangeAt(0).cloneContents();

  e.clipboardData.setData('text/html', contents);

  /* Modify nodes of the contents here */

  // Replace images with their alt-text
  let imgs = contents.querySelectorAll('img');
  for (let x = 0; x < imgs.length; x++) {
    imgs[x].parentNode.replaceChild(
      document.createTextNode(imgs[x].alt),
      imgs[x]
    );
  }

  let text = contents.textContent;

  /* Modify the text as you like here */

  e.clipboardData.setData('text/plain', text);
  e.preventDefault();
}, false);

The good news is that we got our alt-text back. The bad news is that we got ALL the text content contained in the nodes we cloned, including the Javascript code inside an inline <script> element! This is definitely not what we want. Here's what the HTML of that sentence you just copied looks like:

Try copying this sentence, <script>let inlineScript = 'Here is some inline Javascript. You should never see this!'; </script>including the inline image, and pasting it into the te<img src="/img/icon/x.svg" alt="[this is the alt text of the inline image]" class="inline-icon-svg">tarea below.

Hrms. Couldn't we use .innerText instead of .textContent though? .innerText is aware that the content of non-visible elements shouldn't be part of the text returned. It would solve all our problems here. Unfortunately, its main strength is also its biggest weakness.

The cloned nodes we're working on are not part of the visible document, and .innerText is styling-aware as well. If we make changes to the nodes, .innerText will try to do a reflow of the contents, but because nothing in the cloned nodes are visible in the document, nothing happens, and innerText displays the original unaltered content. We would have to quickly add our altered node content to the visible document, take an .innerText reading, then remove it again. And for large selection areas, that might weigh HEAVILY on your user's resources!

So instead we use .textContent which doesn't require a reflow, but returns all the text content of our invisible node collection, even nodes we might not want. Is there a way to fix this too?

If we can come up with a list of values for .querySelectorAll() that can remove all those nodes, we can just run that on every selection. We can even add our own custom values for any special situations that might pop up on our websites. The code ends up looking like this. I even added a couple standard Bootstrap classes that hide elements.

document.addEventListener('copy', function(e) {
  let contents = window.getSelection().getRangeAt(0).cloneContents();

  // Remove all nodes that shouldn't be visible
  let invisiNodes = [
    'head',
    'script',
    '.d-none',
    '.invisible',
    'noscript',
    // Add more as you like
  ];
  for (let x = 0, n; x < invisiNodes.length; x++) {
    n = contents.querySelectorAll(invisiNodes[x]);
    for (let y = 0; y < n.length; y++) {
      n[y].parentNode.removeChild(n[y]);
    }
  }

  e.clipboardData.setData('text/html', contents);

  /* Modify nodes of the contents here */

  // Replace images with their alt-text
  let imgs = contents.querySelectorAll('img');
  for (let x = 0; x < imgs.length; x++) {
    imgs[x].parentNode.replaceChild(
      document.createTextNode(imgs[x].alt),
      imgs[x]
    );
  }

  let text = contents.textContent;

  /* Modify the text as you like here */

  // Update the text of our copied message
  text = text.replace(/Try/, 'You succeeded in');
  text = text.replace(/image, and/, 'image, excluding the inline script, and');

  e.clipboardData.setData('text/plain', text);
  e.preventDefault();
}, false);

Does it Work?

Try copying this sentence, including the inline image, and pasting it into the te[this is the alt text of the inline image]tarea below.

Hint: Be sure to copy the entire sentence beginning from "Try".


Comments closed

Recent posts

  1. Customize Clipboard Content on Copy: Caveats Dec 2023
  2. Orcinus Site Search now available on Github Apr 2023
  3. Looking for Orca Search 3.0 Beta Testers! Apr 2023
  4. Simple Wheel / Tire Size Calculator Feb 2023
  5. Dr. Presto - Now with MUSIC! Jan 2023
  6. Archive