Customize Clipboard Content on Copy: Caveats
Dec 14, 2023A 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:
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.
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.
Try copying this sentence, including the inline image, and pasting it into the tetarea 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.
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?
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.
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);
Hint: Be sure to copy the entire sentence beginning from "Try".
⇐ Orcinus Site Search now available on Github |