Reusable JavaScript Click-to-Copy

JavaScript click-to-copy tutorial

Add click-to-copy JavaScript functionality to any WordPress site. Use it with any element (or WordPress block) just by setting a CSS class name. It’s ultra-lightweight and should work with page builders too.

Project requirements

  • The code needs to work with any WordPress project just by adding it to a child theme – no complicated configuration
  • Use native JavaScript so we don’t introduce any external dependencies (such as jQuery)
  • Built-in styling, but it can be custom-styled on a per-project basis
  • Usually we’ll want to click-to-copy an element’s on-screen text, but we might also want to click on an icon and copy some text
  • Use CSS classes to enable click-to-copy, so the code should work with the WordPress Block Editor, Elementor and other page builders

We’re going to make this

Some interesting text

More interesting text

click the buttons to copy the text

Examples

Try clicking on the examples and pasting the result into a text editor…

click-to-copy CSS class in a WordPress block

Example one

lorem ipsum dolor sit amet

Click on this

Add “click-to-copy” to a WordPress Paragraph Block to enable basic click-to-copy. There’s no CSS styling, so you can make it look how ever you want.

Stylised click-to-copy WordPress block

Example two

lorem ipsum dolor sit amet

Click on this

Add the “fancy-click-to-copy” class to a Paragraph Block to apply some layout that should look familiar to users.

<a data-ctc="Stop" class="click-to-copy">
	<i class="fas fa-stop"></i>
</a>
<a data-ctc="Play" class="click-to-copy">
	<i class="fas fa-play"></i>
</a>
<a data-ctc="Pause" class="click-to-copy">
	<i class="fas fa-pause"></i>
</a>

Example three

Stop Play Pause

Click on these

Put your “copy text” into the data-ctc="..." attribute to specify what text should be copied when clicked. Useful when the clickable element is an icon, so the element’s innerText property is empty. You still need to add “click-to-copy” to the element’s classes.

How it’s going to work

The core functionality will be implemented in native JavaScript, with logic like this:

When the document has loaded

  • Scan the DOM for all elements that have a class of “click-to-copy” or “fancy-click-to-copy”
  • For each element we find…
    • If the element does not have the “click-to-copy” class, add it now
    • If the element’s data-ctc="..." attribute is not set, then…
      • Create a data-ctc="..." attribute and copy the element’s innerText into the attribute
    • Attach a “click” event handler to the element

The click event handler

  • The target of the click event might not be our click-to-copy element (it could be a child element). So we need to find the “closest” element in the DOM that has the “click-to-copy” CSS class and store this in a variable called container
  • If container already has a tooltip then…
    • Don’t do anything
  • Else if the element’s data-ctc="..." attribute is empty (i.e. there is no text to copy) then…
    • Don’t do anything
  • Else…
    • Copy the contents of data-ctc="..." to the clipboard
    • Create a small tooltip element (div) and add it to container as a child element
    • Set a timeout to remove the tooltip after 1.5 seconds (1,500 milliseconds) – configurable

Featured plugin

Product Variation Pills for WooCommerce
Replace the WooCommerce product variation drop-downs with user-focused, mobile-friendly radio/pill buttons. Create a cleaner product page and boost the user experience of your store!
Product radio buttons for WooCommerce

Let’s write the code

In your WordPress child theme, create three empty files called “wpt-click-to-copy.php”, “wpt-click-to-copy.css” and “wpt-click-to-copy.js”. Open wpt-click-to-copy.php and paste the following into it:

<?php

/**
 * Headwall WP Tutorials Click-to-Copy : WPTCTC
 *
 * https://wp-tutorials.tech/refine-wordpress/reusable-javascript-click-to-copy/
 *
 * Changelog
 *
 * 2023-12-19 : Added the 'wpctc_is_required' filter to make it easier to
 *              control which pages should include the click-to-copy CSS & JS.
 */

//  Block direct access
defined('WPINC') || die();

const WPTCTC_TOOLTIP_TIMEOUT = 1500; // milliseconds

/**
 * Control what public-facing content should include our click-to-copy code.
 * By default, we want it on all pages/posts/products but not the front page.
 * If you want access to click-to-copy on EVERY page on your site,
 * return true from here.
 *
 * @return bool     Whether or not to include click-to-copy frontend assets.
 */
function wptctc_is_required() {
	$is_required = is_singular() && !is_front_page();

	return (bool) apply_filters('wpctc_is_required', $is_required);
}

/**
 * Maybe enqueue our JS & CSS assets, and configure our
 * global JS object, wptClickToCopy
 */
function wptctc_enqueue_scripts() {
	if (wptctc_is_required()) {
		$theme_version = wp_get_theme()->get('Version');
		$base_uri = get_stylesheet_directory_uri();

		$handle = 'wptcsc';

		wp_enqueue_style(
			$handle,
			$base_uri . '/wpt-click-to-copy.css',
			null, // No style dependencies
			$theme_version
		);

		wp_enqueue_script(
			$handle,
			$base_uri . '/wpt-click-to-copy.js',
			null, // No script dependencies
			$theme_version
		);

		// Inject a script element that creates a global JavaScript object called
		// "wptClickToCopy" with some useful parameters for our JS code.
		wp_localize_script($handle, 'wptClickToCopy', [
			'tipCopied' => __('Copied', 'wp-tutorials'),
			'tipTimeout' => WPTCTC_TOOLTIP_TIMEOUT,
		]);

		// If you want to enqueue some of your own styles after the click-to-copy
		// assets have been enqueued, you can hook this action in your child theme.
		do_action('wptctc_enqueued_assets');
	}
}
add_action('wp_enqueue_scripts', 'wptctc_enqueue_scripts');

The PHP code is a standard structure for mini JavaScript projects like this. All it needs to do is decide when to include our CSS & JS assets, and pass some parameters to the JS code using wp_localize_script().

To pull the code into your site, open your child theme’s functions.php and add the following couple of lines:

// Headwall WP Tutorials : Click-to-copy
require_once dirname(__FILE__) . '/wpt-click-to-copy.php';

Next, open wpt-click-to-copy.css and add the following style definitions:

/**
 * Headwall WP Tutorials Click-to-Copy : WPTCTC
 *
 * https://wp-tutorials.tech/refine-wordpress/reusable-javascript-click-to-copy/
 *
 */

.click-to-copy {
	cursor: pointer;
	position: relative;
}

/**
 * Tool-tip style
 */
.click-to-copy .ctc-tip {
	cursor: default;
	pointer-events: none;

	position: absolute;
	background-color: #fff;
	border: 2px solid lightgrey;
	font-size: 80%;
	border-radius: 0.5em;
	width: auto;
	left: 50%;
	top: 50%;
	transform: translate(-50%, -50%);
	padding: 0.5em 1em;
	line-height: 1em;
	text-align: center;
	font-style: italic;
	box-shadow: 0 0 1em #8888;
	white-space: nowrap;
	z-index: 10;
	color: black;
}

/**
 * The container for our "fancy" click-to-copy box.
 */
.fancy-click-to-copy {
	background-color: white;
	border-radius: 10px;
	overflow: hidden;
	border: 1px solid lightgrey;
	text-align: center;
	padding: 0 1em 0 2em;
	line-height: 2.5em;
	white-space: nowrap;
}

/**
 * Use Font Awesome 5 to add the icon to the left of the box.
 * If your site does not have FA5, you should change the font-family
 * to the equivalent that works on your site.
 */
.fancy-click-to-copy::before {
	font-family: 'Font Awesome 5 Free';
	font-weight: 900;
	content: '\f0c5';

	display: inline-block;
	position: absolute;
	left: 0;
	top: 50%;
	transform: translateY(-50%);
	transition: 0.3s color;
	border-right: 1px dotted darkgrey;
	padding: 0 1em;
	background-color: #eee;
	color: #666;
	z-index: 10;
}

.fancy-click-to-copy:hover::before {
	color: #3b0081;
}

/**
 * Useful when adding fancy-click-to-copy to a "code" block.
 */
.wp-block-code.fancy-click-to-copy code {
	display: unset;
	font-family: 'monospace';
	overflow-wrap: unset;
	white-space: unset;
}

The tool tip style is controlled by .click-to-copy .ctc-tip near the top. Everything else is for the fancy box, so you can change it to suit your needs. In particular, the above makes reference to Font Awesome 5 for the icon. This should work fine on most projects, but you might need to modify the CSS if the icon doesn’t work for you.

The core JavaScript

Finally, we need to add the core logic to wpt-click-to-copy.js:

/**
 * Headwall WP Tutorials Click-to-Copy : WPTCTC
 *
 * https://wp-tutorials.tech/refine-wordpress/reusable-javascript-click-to-copy/
 *
 * Changelog
 *
 * 2023-12-19 : Added support for a "copy-html" CSS class, so the element's
 *              innerHTML is used to puplate dataset.ctc (instead of innerText).
 *              To copy HTML, add "click-to-copy copy-html" or
 *              "fancy-click-to-copy copy-html" CSS classes to your element.
 *
 *              Added support for legacy copy-text when navigvator.clipboard is
 *              not available. This is useful as a fallback when running the code
 *              over "http" instead of "https".
 */

document.addEventListener('DOMContentLoaded', function() {
	'use strict';

	// Diagnostics
	// console.log('WPT Click-to-copy : load');

	// Only proceed if wptClickToCopy has been specified as a
	// global JS variable.
	if (typeof wptClickToCopy !== 'undefined') {
		/**
		 * Find all elements with either "click-to-copy" or "fancy-click-to-copy"
		 * class and configure them.
		 */
		wptClickToCopy.init = () => {
			// Diagnostics
			// console.log('WPT Click-to-copy : init');

			document.querySelectorAll('.click-to-copy, .fancy-click-to-copy').forEach(function(container) {
				// Make sure the element has "click-to-copy" as one of its classes,
				// as it might only have fancy-click-to-copy.
				if (!container.classList.contains('click-to-copy')) {
					container.classList.add('click-to-copy');
				}

				// The actual text being copied is in the data-ctc="..." attribute.
				// If this hasn't been set, copy the element's inner text (or inner
				// HTML) into the data attribute now.
				if (typeof container.dataset.ctc !== 'undefined') {
					// the click-to-copy text has already been specified, so we don't
					// need to do anything.
				} else if (container.classList.contains('copy-html')) {
					container.dataset.ctc = container.innerHTML;
				} else {
					container.dataset.ctc = container.innerText;
				}

				// Add the element's click event handler.
				container.addEventListener('click', wptClickToCopy.clickHandler);
			});
		};

		/**
		 * Handle click events on our click-to=-copy elements.
		 *
		 * @param  object event   JavaScript event
		 */
		wptClickToCopy.clickHandler = (event) => {
			event.preventDefault();

			// The user might have clicked on a child of the click-to-copy element
			// (or on the tool tip) sowe need to use .closest() to recurse up the
			// DOM and find the correct element. e.g. this could be event.target,
			// event.target.parentElement or even event.target.parentElement.parentElement
			let container = event.target.closest('.click-to-copy');
			let copyText = '';

			if (!container) {
				// The clicked element doesn't have the "click-to-copy" class.
			} else if (wptClickToCopy.hasTip(container)) {
				// The container already has a tip showing.
			} else if ((copyText = container.dataset.ctc).length == 0) {
				// There's no text to copy (empty string)
			} else {
				// If the site is using HTTPS then we can use navigator.clipboard to
				// copy the text. Otherwise we need to create a temporary textarea
				// element, populate it and call document.execCommand('copy')...
				// it's a bit of a fiddle, but it should work in older browsers and
				// with non-https connections.
				if (navigator.clipboard && window.isSecureContext) {
					navigator.clipboard.writeText(copyText);
				} else if (typeof document.execCommand === 'function') {
					const textArea = document.createElement('textarea');
					textArea.value = copyText;
					textArea.style.position = 'absolute';
					textArea.style.left = '-100px';
					textArea.style.top = '0px';
					textArea.style.width = '50px';
					document.body.prepend(textArea);

					try {
						textArea.focus();
						textArea.select();
						document.execCommand('copy');
					} catch (error) {
						console.error(error);
					} finally {
						textArea.remove();
					}
				} else {
					console.error('Unable to copy the text');
				}

				// Add a temporary tooltip to the click-to-copy element, using the
				// contents of wptClickToCopy.tipCopied as the label (passed from
				// the back-end PHP).
				wptClickToCopy.addTip(container, wptClickToCopy.tipCopied);
			}
		};

		/**
		 * Utility function that tells us if an element already has a tooltip,
		 * or not.
		 *
		 * @param  object   container   The click-to-copy element.
		 * @return bool                 Does this element have a child tooltip
		 */
		wptClickToCopy.hasTip = (container) => {
			return container.querySelectorAll('.ctc-tip').length > 0;
		};

		/**
		 * Remove all child tool tips from this element.
		 *
		 * @param  object   container   The click-to-copy element.
		 */
		wptClickToCopy.removeTip = (container) => {
			container.querySelectorAll('.ctc-tip').forEach((tip) => {
				// console.log(`Remove: ${tip.innerText}`);
				tip.remove();
			});
		};

		/**
		 * Add a tool tip to this element.
		 *
		 * @param  object   container   The click-to-copy element.
		 * @param  string   tipText     The text to show in the tool tip
		 */
		wptClickToCopy.addTip = (container, tipText) => {
			const tip = document.createElement('div');
			tip.classList.add('ctc-tip');
			tip.append(tipText);
			container.append(tip);

			// After the number of milliseconds specified in "wptClickToCopy.tipTimeout",
			// remove the tool tip form the DOM.
			if (wptClickToCopy.tipTimeout > 0) {
				setTimeout(() => {
					tip.remove();
				}, wptClickToCopy.tipTimeout);
			}
		};

		/**
		 * Main entry point.
		 */
		wptClickToCopy.init();
	}
});

If you were to take out the comments, there wouldn’t be much code left in here. It was tempting use jQuery to create animated tooltips (or something like the Popper JS library). But I think it’s better to keep things really lean here because the code can end up on lots of pages… and we want to minimise impact on page-load speeds.

Testing

Make sure everything is saved and try adding “fancy-click-to-copy” to a paragraph block that’s got some text in it. Save the content and check it out in the front-end.

If it doesn’t seem to work, edit the JavaScript, uncomment the Diagnostics lines and enable your browser’s Dev Tools… then reload the page.

// Diagnostics
console.log('WPT Click-to-copy : load');

On reload, you should see WPT Click-to-copy : load in the output. If it’s not there your assets aren’t being enqueued, so you should check the return value of wptctc_is_required() in “wpt-click-to-copy.php”. Try setting it to always return true so the assets are enqueued on every page.

Example click-to-copy paragraph block
Create a paragraph block

Have fun clicking and copying ๐Ÿ˜Ž ๐Ÿ‘

Like This Tutorial?

Let us know

WordPress plugins for developers

2 thoughts on “Reusable JavaScript Click-to-Copy”

  1. This came in very handy, thank you! I modified slightly so I could copy the HTML rather than just the text โ€”ย instead of:

    if ((copyText = container.dataset.ctc).length == 0)

    I wrote

    if ((copyText = container.innerHTML).length == 0)

    Have not tested extensively yet to be sure this will work, but I suspect it should.

    Reply
    • I’ve had a look through the code again and I think we need to keep using container.dataset.ctc, but I’ve put your suggestion into the JS init() function. So, if the container element has got the “copy-html” CSS class then we use innerHTML to populate dataset.ctc, otherwise we use innerText to populate dataset.ctc as the fallback. I’ve also put some code in there to fallback to an alternative copy-text method if the navigator.clipboard object isn’t available.

      To use it, just use “click-to-copy copy-html” as your element’s CSS class ๐Ÿ‘

      Reply

Leave a comment