Not long ago, I was looking for a way to copy the content of any HTML element on a page to clipboard. What I thought would be an easy task turned out to be more complicated than expected. In the end I had to use some trick which I will show here.
The way this will work is as follows: when clicking an element on the page, its innerText will be copied to Clipboard. To achieve this we will need to:
- Attach an
onclickevent listener to our element (or to several elements) - When a
clickevent is received, emit a fakecopyevent
// This variable will be used to store a reference to the
// latest element clicked.
let selected = null;
// Attach our custom copy-to-clipboard-on-click trick to `elt`
const elt = document.querySelector('...');
elt.onclick = () => {
// Store reference to this element in the global `selected`
selected = elt;
// Emit fake `copy` event
document.execCommand('copy');
// Remove reference from `selected` to allow normal copies
// to be performed on the page.
setTimeout(() => { selected = null; }, 1000);
};
- Listen to this
copyevent and copy the content of the latest element clicked to clipboard.
// Intercept copy events
document.addEventListener('copy', (e) => {
if (selected === null) {
// No element was clicked
return;
}
// Copy content of clicked element to clipboard
e.clipboardData.setData(
'text/plain',
selected.innerText,
);
// We want our data, not data from any selection,
// to be written to the clipboard
e.preventDefault();
});
2 comments
Interesting. Ran into this problem myself recently with a file upload interface and “copy short link” actions, excepting that I did not want the URI being copied present on the page. (It technically is, but in an abbreviated form that would not produce a viable URI. The “slashes” aren’t: they’re
/or a Unicode FULLWIDTH SOLIDUS.)My solution involved a “staging area” plain text input to use to place the value to be copied within. If that sentence can even English.
sample.html<!-- A place to use as the origin of the copy operation. --> <form> <textarea id=clipboard></textarea> </form> <!-- Some thing to copy. --> <li data-uri=https://flffy.app/CoaE_HfG_uPXUI_d/screenshot.png> <label>flffy.app/CoaE_HfG_uPXUI_d/screenshot.png</label> <a class="copy fad fa-link">Copy Link</a> </li>The CSS
display: noneandvisibility: hiddenoften have unintentional side-effects, especially thedisplayone, so we utilize other means to make our input invisible. (We don’t want to prevent pseudo-keyboard—shortcut—interaction automation from working.)sample.csstextarea#clipboard { width: 1px; height: 1px; opacity: 0; position: absolute; top: 0; left: 0 }Use of a “staging” hidden input field (visually; not literally
type=hidden) ensures rich text media will not be sent to the clipboard. (Always love pasting “Read more…” that happens to be a link, instead of the link, into a non-rich-text input… and you did indeed read that right.) This can be especially egregious if font, font color, and background information gets copied.The technique is otherwise similar in that it utilizes the same underlying
execCommandinvocation, but without having to intercept such invocations to manipulateclipboardDatadirectly:sample.js// Attaching to the body allows capture of "bubbled" events. // It saves worrying about binding the handler to each individually, and // to any dynamic elements as they are added. document.body.addEventListener('click', e => { // We "exit early" if the element clicked isn't of interest to us. if ( !e.target.classList.contains('copy') ) return let elem = e.target.matches('li[data-uri]') ? e.target : e.target.closest('li[data-uri]') if ( !elem ) return // My use-case: copy the data-uri property from the clicked // element... or the nearest parent element having one. clipboard.innerText = elem.dataset.uri clipboard.select() document.execCommand("copy") clipboard.blur() })Note also several shortcuts utilized: arrow functions because they’re darned nice, like your sample, but with a notable lack of
getElementByIddespite using an HTML element for the stage… HTML elements identified with JavaScript symbol-safe names are exposed as attributes of thewindowobject: global variables. Also: who needs semicolons? 😉Visually, the prototype looked like: (in dark mode)
One deficit that could be corrected: this does not restore any prior focus or selection… but it could be extended to. Edited to add: though an arbitrary “one second timeout” to do so would be exceptionally sub-optimal, IMO. Similar to link analytics services like Google Analytics literally delaying every link on your page by 200ms just to ensure the XHR gets out. Not good! 😜
Very nice. Thank you for sharing! Regarding the timeout, I am not sure why I did not simply clear the global reference in the ‘copy’ listener, I guess it should always trigger…