../

Copy anything to clipboard in HTML and JavaScript

└─ 2018-06-09 • Reading time: ~1 minutes

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 fake copy 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();
  });

Leave a comment on GitHub
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 and visibility: hidden often have unintentional side-effects, especially the display 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 manipulate clipboardData 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 the window object: global variables. Also: who needs semicolons? 😉

    Visually, the prototype looked like: (in dark mode)

    Screen Shot 2020-07-03 at 22 39 41

    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! 😜

  • GitHub avatar from user remusao remusao commented 4 years ago

    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…