home ~ projects ~ socials

Create A Copy Button For Code Blocks With Vanilla JavaScript

TODO

Look at replacing the function with this

function addCopyButtonTo(codeSelector, buttonParentSelector) {
  const codeEl = document.querySelector(codeSelector)
  const buttonParentEl = document.querySelector(buttonParentSelector)
  const copyButton = document.createElement("button")
  copyButton.innerHTML = "Copy"
  copyButton.dataset.target = codeSelector
  copyButton.addEventListener("click", async (event) => {
    const elToCopy = document.querySelector(event.target.dataset.target)
    console.log(elToCopy)
      try {
        let content
        if (elToCopy.value) {
          content = elToCopy.value
        } else {
          content = elToCopy.innerText
        }
        await navigator.clipboard.writeText(content)
        event.target.innerHTML = "Copied"
      } catch (err) {
        event.target.innerHTML = "Error copying"
      }
      setTimeout((theButton) => {
        event.target.innerHTML = "Copy"
      }, 2000, event.target)
  })
  buttonParentEl.appendChild(copyButton)
}

Introduction

This is how I'm adding "Copy" buttons to code blocks.

The HTML

Everything starts with a

with a specific class that wraps the
 tags that contain the code. In this case, I'm using code-copy-example for this live example:

print("this is some code")

print("with a few lines")

The JavaScript

The button in the Output is added via JavaScript. It works by looking for any elements with the code-copy-example class and dynamically adding the button to them with this JavaScript:

JavaScript

function addCodeCopyButtonsExample() {
  console.log("Adding example code copy buttons")
  const codeExamples = document.querySelectorAll(".code-copy-example")
  codeExamples.forEach((example, index) => {
    const dataId = `block-${index}`
    example.dataset.codeblockexample = dataId
    const copyButton = document.createElement("button")
    copyButton.innerHTML = "Example Copy Button"
    copyButton.classList.add("code-copy-example-button")
    copyButton.dataset.codeblockexamplebutton = dataId
    copyButton.addEventListener("click", async (event) => {
      const el = event.target
      const captureId = el.dataset.codeblockexamplebutton
      console.log(`Copying example code block: ${captureId}`)
      const codePreEl = document.querySelector(
        `[data-codeblockexample="${captureId}"] pre`
      )
      try {
        await navigator.clipboard.writeText(
          codePreEl.innerText
        )
        el.innerHTML = "Copied"
      } catch (err) {
        el.innerHTML = "Error copying"
      }
      setTimeout((theButton) => {
        theButton.innerHTML = "Example Copy Button"
      }, 2000, el)
    })
    example.appendChild(copyButton)
  })
}

document.addEventListener("DOMContentLoaded", () => {
  addCodeCopyButtonsExample()
})

The CSS

The last component of the setup is to position the button in the upper right corner with this CSS:

CSS

.code-copy-example {
  position: relative;
}

.code-copy-example-button {
  position: absolute;
  right: 0;
  top: 0;
}

Wrapping Up

I really like this approach. It uses Progressive Enhancement1 so the Copy buttons don't show up unless JavaScript is working. That's compared to hard coding the buttons in the HTML where they would show up regardless of if the JavaScript to power them was working or not.

-- end of line --