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
onclick
event listener to our element (or to several elements) - When a
click
event is received, emit a fakecopy
event
// 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
copy
event 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: none
andvisibility: hidden
often have unintentional side-effects, especially thedisplay
one, so we utilize other means to make our input invisible. (We don’t want to prevent pseudo-keyboard—shortcut—interaction automation from working.)sample.css
textarea#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
execCommand
invocation, but without having to intercept such invocations to manipulateclipboardData
directly: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
getElementById
despite using an HTML element for the stage… HTML elements identified with JavaScript symbol-safe names are exposed as attributes of thewindow
object: 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…