150

How to Add a Copy-to-Clipboard Button to Jekyll

 4 years ago
source link: https://www.aleksandrhovhannisyan.com/blog/how-to-add-a-copy-to-clipboard-button-to-your-jekyll-blog/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

How to Add a Copy-to-Clipboard Button to Jekyll

I’m always looking for ways to improve my site’s user experience without toppling the precarious house of cards that is cross-browser compatibility. And one thing that recently drew my attention is the fact that many of my tutorials require copy-pasting code, especially for anything that’s not too important to type out by hand (e.g., terminal commands).

You’re working with Jekyll, so you’re probably using Markdown code blocks like this:

Run this command to get started:

```bash
cd my-awesome-project && npm install
```

And that works, sure. But copy-pasting this can get tedious really quickly, and it’s barely accessible. Why not create a button that magically copies Markdown code blocks to your clipboard?

Well, ask and you shall receive! In this tutorial, we’ll add a copy-to-clipboard button to your Jekyll blog in just a few lines of code. Here’s a sneak peek at what we’ll be building:

A demo of clicking a copy-to-clipboard button.

Psst! You can also try this out live on my blog!

Note that this tutorial won’t introduce any optional CSS or HTML. I’m just going to show you how to get this thing working at a functional level. Once that’s taken care of, you can throw in any extra styling or elements that you want.

Copy-to-Clipboard Button in Jekyll with Liquid and JavaScript

Let’s run through how this is going to work at a high level:

  1. We’ll define the code that we want to copy and store that in a variable.
  2. We’ll pass in the code as an argument to an include.
  3. The include file will define the copy-to-clipboard button and the code block.
  4. The copy-to-clipboard button will get a data-code attribute with a copy of the code.
  5. Special characters like quotes will be escaped in the code string.

Sound good? Let’s first look at the include file itself:

1. Copy-to-Clipboard Include: _includes/code.html

_includes/code.html
<div class="copy-code-container">
    <button class="copy-code-button"
         aria-label="Copy code block to your clipboard"
         data-code="{{ include.code | escape }}"
    ></button>
</div>
```{{ include.lang }}
{{ include.code }}
```

Note: If you need to show fenced code blocks in your tutorials and want the triple backticks to show up as-is in the output HTML (e.g., like I did above), you should use four backticks in your code.html file. That way, the nested include.code object (which is assumed to house the triple backticks) won’t break the Markdown processor.

We create a button and give it a well-named class. We also add an aria-label for screen readers.

Below is the essential CSS (I’m using SCSS). Feel free to change this to suit your needs. Note that some of this will make more sense once we make the copy-to-clipboard button interactive with JavaScript.

.copy-code-container {
  display: flex;
  justify-content: flex-end;
  padding: 1em;
  background: #3b3b3b;
}

.copy-code-button {
    display: flex;
    align-items: center;
    justify-content: center;
    border: none;
    cursor: pointer;
    font-size: 1rem;
    background-color: #616161;
    color: #e7e7e7;
    padding: 0.4em 0.5em;
    border-radius: 5px;

    &::before {
        content: "Copy";
    }

    &::after {
        margin-left: 4px;
        content: "📋";
        width: 1em;
    }

    // This class will be toggled via JavaScript
    &.copied {
        &::before {
            content: "Copied!";
        }

        &::after {
            content: "✔️";
        }
    }
}

If you take a closer look at _includes/code.html, you’ll notice this interesting data-code attribute:

data-code="{{ include.code | escape }}"

Later on, we’re going to pass in a block of code with newlines, quotation marks, and all sorts of other characters as-is. Here, we use the escape Liquid filter to escape any characters in our code.

Here’s a real example, from this very blog post, of what the escaped code would look like in HTML:

Inspecting an HTML element with Chrome dev tools.

Cool! That way, the embedded quotes in the code don’t break our HTML. And since we’ve stored the code in an HTML attribute, we can use JavaScript to copy that to the clipboard later on.

This part is hopefully self-explanatory:

```{{ include.lang }}
{{ include.code }}
```

Let’s pretend that we’ve already stored our code as a literal string in a variable named code (we’ll see how that’s done in the next section). We would invoke the include like so:

{% include code.html code=code lang="javascript" %}

These two arguments will become accessible under include.code and include.lang, respectively. So when Jekyll goes to evaluate the Liquid template, it’ll perform the following substitutions:

_includes/code.html
<div class="copy-code-container">
    <button
      class="copy-code-button"
      aria-label="Copy code block to your clipboard"
      data-code="const foo = new Bar();"
    ></button>
</div>
```javascript
const foo = new Bar();
```

This begs the following question: How do we store the code in a string variable?

How do we… say, capture it?

2. Liquid Capture Tag to the Rescue

Let’s say you want to store the following code in a variable, with all special characters preserved:

const foo = new Bar();
foo.doDangerousJavascriptThings();
console.log("I love Jekyll!");

You may be tempted to copy-paste this code into a string and assign that to a variable:

{% assign code = "const foo = new Bar();
foo.doDangerousJavascriptThings();
console.log("I love Jekyll!");" %}

Unfortunately, that won’t work because our code contains quotation marks. That may not be too big of a deal because we could escape them. But a bigger problem is that the newlines will be consumed, and what you’ll actually get is a one-liner.

To create a multi-line string in Jekyll, we need to use the Liquid capture tag, like so:

{% capture code %}const foo = new Bar();
foo.doDangerousJavascriptThings();
console.log("I love Jekyll!");{% endcapture %}

Everything inside the capture tag is stored in a multi-line string and can later be referenced by the variable code (you could name this something else if you wanted to, by the way).

Note: I recommend that you keep the capture start tag ({% capture code %}) inline with the first line of code and the end tag ({% endcapture %}) inline with the last line of code. If you don’t, your code will get two extra newlines: one at the start and one at the end.

If this seems like too much work, it really isn’t. There are only three simple steps:

  1. Type {% capture code %}{% endcapture %}.
  2. Place your cursor between the start and end tags.
  3. Paste in your code!

What if your blog post is about Jekyll and your code contains Liquid templates, like some of the code in this blog post does? In that case, simply do:

{% capture code %}{% raw %}...{% endraw %}{% endcapture %}

And just replace the ellipsis with your code. Once you’ve captured the code, simply use the include:

{% include code.html code=code lang="javascript" %}

Rinse and repeat for every code block that you want to insert into your blog post!

Making Our Lives Easier with a Custom Snippet

Of course, all of that may still seem like a lot of work… Which is unfortunate—because while we’re trying to make things easier for the user, we’re making things harder for ourselves!

If you’re using VS Code, you can set up a really handy snippet to save yourself time. If you’re using another editor, such as Sublime Text or Atom, there’s a site that generates snippets for those, too.

On VS Code, open up your command palette (Ctrl+Shift+P on Windows and Command+Shift+P on Mac), type user snippets, and select Preferences: Configure User Snippets. Then type markdown in the search bar, and VS Code will open up a settings file for Markdown files. Insert this snippet:

markdown.json
{
	"Code Snippet": {
		"prefix": "code",
		"body": [
			"{% capture code %}$1{% endcapture %}",
			"{% include code.html code=code lang=\"$0\" %}"
		]
	}
}

Then, in any Markdown file, all you’ll have to do is type the prefix code and hit Enter; VS Code will auto-insert this snippet for you and even move your cursor so it’s between the capture tags. This reduces a potentially tedious process to just a few keystrokes. Paste in your code, tab over to lang="" to specify the language, and you’re all set!

Note: If you’re not seeing any suggestions when you type in Markdown files, the solution is to enable quick suggestions for Markdown in your VS Code user settings.

3. Copying to the Clipboard with JavaScript

Awesome! We’re already two-thirds of the way done. Now, all that’s left is the JavaScript to actually do the copying. And this is actually the easy part.

We’ll start by defining a skeleton for the function:

assets/scripts/copyCode.js
const copyCode = (clickEvent) => {
  // The magic happens here
};

document.querySelectorAll('.copy-code-button').forEach((copyCodeButton) => {
  copyCodeButton.addEventListener('click', copyCode);
});

Pretty straightforward! We’re just registering click handlers on all of the buttons on the current page.

The meat of the copy-to-clipboard logic is taken from this StackOverflow answer, which creates a temporary textarea element, adds it to the DOM, executes document’s copy command, and then removes the textarea:

assets/scripts/copyCode.js
const copyCode = (clickEvent) => {
  const copyCodeButton = clickEvent.target;
  const tempTextArea = document.createElement('textarea');
  tempTextArea.textContent = copyCodeButton.getAttribute('data-code');
  document.body.appendChild(tempTextArea);

  const selection = document.getSelection();
  selection.removeAllRanges();
  tempTextArea.select();
  document.execCommand('copy');
  selection.removeAllRanges();
  document.body.removeChild(tempTextArea);

  // TODO more stuff here :)
};

document.querySelectorAll('.copy-code-button').forEach((copyCodeButton) => {
  copyCodeButton.addEventListener('click', copyCode);
});

That’s really all you need to get this to work, but I also have the following on the TODO line:

copyCodeButton.classList.add('copied');
  setTimeout(() => {
    copyCodeButton.classList.remove('copied');
}, 2000);

My copy-to-clipboard button uses CSS pseudo-elements to show Copy 📋 in the default state and Copied! ✔️ once the copied class has been added. This lasts for two seconds and gives the user feedback to indicate that copying to the clipboard went through successfully.

That’s it! Don’t forget to add a script tag so this code actually works. For example, you can stick this somewhere in your layout file for blog posts:

_layouts/post.html
<script src="/assets/scripts/copyCode.js"></script>

Further Improvements: File Name and Copy-to-Clipboard Button

Of course, you may have noticed that the code blocks on my own website have four variants:

  1. No code block header (no file name or copy-to-clipboard button). These are your standard Markdown code blocks rendered with triple backticks.
  2. Code block header with the file name only (for illustrative code that shouldn’t be copied).
  3. Copy-to-clipboard button only (whenever there isn’t a file name, like for terminal commands).
  4. Both a file name and a copy-to-clipboard button.

This layout isn’t actually as complicated as it may seem; you just need a top-level wrapper div for the header, with two nested flex containers: one for the file name and another for the button:

Inspecting the code block headers on my website

All of these variations still use the same include file that we looked at in this blog post, except some elements are conditionally rendered based on a flag parameter that I pass in whenever I don’t want to render something. Here’s what my full _includes/code.html file looks like:

_includes/code.html
<div class="code-header">
    {% if include.file %}
    <div class="code-file-name-container">
        <span class="code-file-name">{{ include.file }}</span>
    </div>
    {% endif %}
    {% unless include.copyable == false %}
    <div class="copy-code-container">
        <button class="copy-code-button"
             aria-label="Copy code block to your clipboard"
             data-code="{{ include.code | escape }}"
             type="button"
        ></button>
    </div>
    {% endunless %}
</div>
```{{ include.lang }}
{{ include.code }}
```

For example, using the unless Liquid tag and a flag passed in as an argument, I can regulate when the copy button should appear:

{% unless include.copyable == false %}
    <div class="copy-code-container">
        <button class="copy-code-button"
             aria-label="Copy code block to your clipboard"
             data-code="{{ include.code | escape }}"
             type="button"
        ></button>
    </div>
{% endunless %}

This means I can later import the file as follows, and it won’t render the copy-to-clipboard button:

{% include code.html code=code lang="myFavoriteLanguage" copyable=false %}

If you’re wondering why I used unless instead of if, it’s because this allows me to specify the default behavior as “always include a copy-to-clipboard button unless I say otherwise.”

Likewise, if I want to specify a file name, I just need to pass in file="myFileName", and that’ll conditionally render the div for the file container with a simple if tag:

{% if include.file %}
    <div class="code-file-name-container">
        <span class="code-file-name">{{ include.file }}</span>
    </div>
{% endif %}

Copy That!

It’s amazing just how much you can get away with in Jekyll using simple Liquid templates and JavaScript! If you’ve been following along, you should now be all set to use copy-to-clipboard buttons in your Jekyll blog posts. I’ll leave it up to you to make the code block header look nicer than what I’ve presented here. Get creative!

Admittedly, this does require some additional (albeit negligible) effort to use in place of regular Markdown code blocks. Refactoring my old posts was a pain, yes, but I’d say it’s worth it if it makes the user experience better on my site. It also makes it easier for me to verify that my tutorials work when followed from scratch since I don’t have to manually copy-paste all of the code.

I hope you enjoyed this tutorial! If you run into any issues implementing this on your own site, let me know down below and I’ll try to help as best as I can.

💬 Comments (16)

Post comment
  1. 49177904?v=4RBrandon8
    commented 2 months ago
    Edited

    I had trouble with copy maintaining new lines when using the just-the-docs theme. With the code as is, it was pasting as one line. I found that changing the line in code.html from:

    data-code="{{ include.code | escape }}"
    

    to this

    data-code="{{ include.code | uri_escape }}"
    

    Then changing the javascript from:

    tempTextArea.textContent = copyCodeButton.getAttribute('data-code');
    

    to this

    tempTextArea.textContent = decodeURI(copyCodeButton.getAttribute('data-code'));
    

    resolved the issue.

  2. 19352442?v=4AleksandrHovhannisyan
    commented 3 months ago
    Author

    @raghavmallampalli Alternatively, I'd recommend putting your JavaScript at the end of your <body> instead of loading it in your <head>. This guarantees that the DOM content has loaded by the time the JavaScript is parsed and executed.

  3. 49435792?v=4raghavmallampalli
    commented 3 months ago

    For those not very comfortable with JS, you might have to wrap the querySelectorAll with a DOMContentLoaded listener. It didn't work for me until I did that, but worked perfectly once I did. Basically, replace the last few lines of copyCode.js with the following

    document.addEventListener("DOMContentLoaded", function(event) {
        document.querySelectorAll('.copy-code-button').forEach((copyCodeButton) => {
            copyCodeButton.addEventListener('click', copyCode);
        });
    });
    
  4. 19352442?v=4AleksandrHovhannisyan
    commented 4 months ago
    Author

    @mark-plummer Hmm, everything looks fine except for this comment that's missing a closing tag:

    image

    Could that be the issue? Try fixing it and let me know. Thanks!

    Also, after you shared your code.html file, I realized that I'd neglected to update the tutorial and get rid of the h6 tags around the optional file name. I recommend replacing those with either a div or a span. Otherwise, you may end up in a situation where your document's heading levels go from, say, h3 to h6 all of a sudden, and that'll hurt your site's accessibility score. I've updated the blog post to reflect this, but I figured I'd let you know as well.

  5. 44984607?v=4mark-plummer
    commented 4 months ago
    Edited

    sure @AleksandrHovhannisyan. I'm attaching my copyScript.js file, code.html include, and source view of the rendered HTML of the page I'm working on. First, here's my markup:

    Request

    {% capture code %}POST /ts_dataservice/v1/public/session HTTP/1.1
    Host: client.mydomain.com Accept: application/json Content-type: application/json { "username":"", "password":"" }{% endcapture %} {% include code.html code=code lang="javascript"%}

    code.html.zip copyCode.js.zip view-source_reference_tsload.html.zip

    I love the fact that your solution can copy multiline (indented) code blocks, unlike the clipboard.js solution, so I'm keeping my fingers crossed that we can get it to work. thanks for any help you can provide!

  6. 19352442?v=4AleksandrHovhannisyan
    commented 4 months ago
    Author

    @mark-plummer Interesting... Nothing wrong there. Do you mind sharing some code samples for the copy script and your markup? Not sure what could be throwing things off, but I'm curious what's going on.

  7. 44984607?v=4mark-plummer
    commented 4 months ago

    Hi @AleksandrHovhannisyan . I am including the script on my layout page (which in my case, is called default.html). I'm including it the following way, based on my directory structure: .

    here is the output of my console: image

  8. 19352442?v=4AleksandrHovhannisyan
    commented 4 months ago
    Author

    @mark-plummer No problem! Did you make sure to include the script on your page? I recommend doing this in your layout file for posts (in my case, that's _layouts/post.html). If you did include the script on the page but you're still running into this problem, could you open up your browser's developer tools (Ctrl+Shift+I on Windows and Cmd+Shift+I on Mac) and share a screenshot of the Console tab?

  9. 44984607?v=4mark-plummer
    commented 4 months ago
    Edited

    Hi Aleksander. thanks so much for putting together this great tutorial. I've been trying to implement it on our Jekyll site following the steps you provided. I place code I want to make copyable between the {% capture code %} and {% endcapture %} tags and then immediately follow it with {% include code.html code=code lang="javascript" %}. The code does render on the page correctly, but for some reason, when I click the Copy button, the code is not copied (and the status inside the button does not change to Copied). Any ideas on what I might be doing wrong? thanks again.

  10. 63548362?v=4talelhub
    commented 7 months ago

    Everything is working perfectly, kudos for the work you have done and Many thanks for your time!

  11. 19352442?v=4AleksandrHovhannisyan
    commented 7 months ago
    Author

    Ah, I would advise against putting the script in your include file (this may be part of the problem, but I'm not sure). This means your <script></script> block will get included on the page as many times as you have code blocks (e.g., if you have 4 code blocks, you'll have 4 duplicate scripts). What you really want is to add the script to your layout file for blog posts (e.g., _layouts/post.html) at the very bottom so that each blog post gets one copy of that script. Let me know if that makes sense!

  12. 63548362?v=4talelhub
    commented 7 months ago

    Thank you for your reply. I saved the file "code.html" in _includes (see attached its content, it contains html code and also js script CopyCode), and the css part in the style file. Then I simply put my codes blocks in the markdown file as follows:

    Paragraph1 Some text {% capture code %}CodeBlock1{% endcapture %} {% include code.html code=code lang="c#" %} Paragraph2 Some text {% capture code %}CodeBlock2{% endcapture %} {% include code.html code=code lang="c#" %}

    code.txt

  13. 19352442?v=4AleksandrHovhannisyan
    commented 7 months ago
    Author

    Hmm, interesting. Are you comfortable sharing a sample of the blog post in question? My only guess as to why this may be happening is that the first {% capture code %}{% endcapture %} is overriding subsequent ones.

  14. 63548362?v=4talelhub
    commented 7 months ago

    Hi Aleksander, many thanks for your reply! Modifying the bottom margin is working perfectly.

    I have another issue, I have few code blocks in the same page and I noticed that the new feature is working for the first one but not for the other ones. When copying the first code block, everything works fine (copy to copied and I can paste somewhere else), but when copying the second one nothing happens (no change from copy to copied and paste disabled). Any recommendations please for multiple code blocks use case?

  15. 19352442?v=4AleksandrHovhannisyan
    commented 7 months ago
    Author

    @talelhub Glad you enjoyed the read!

    One of the ways you could do this is by giving your .copy-code-container div a solid background color (and maybe setting its width to be 100%—not entirely sure if you need to). If there's still a gap between it and the code block below, you can give it a negative bottom margin equal to the gap. I currently do something similar on my own site.

    If that still doesn't work, could you inspect .copy-code-container your browser's dev tools and show me a screenshot?

  16. 63548362?v=4talelhub
    commented 7 months ago

    Hi Aleksander, Thank you for this amazing explanation. I tried to add the feature, I have an issue with style. I would like to delete the blank space between the geader containing the copy button and the code block so the button appear as an integrated component within the code block. Would you have recommendations to do so? Many thanks in advance for your help.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK