Crafting a copy button for Earmark generated markup

Earmark is a powerful tool for parsing markdown, here we used it to create a simple blog. Today, we’ll use javascript and browser API to create a copy feature, it will create a button that copies code tag’s content generated by a markdown parser. You can clone the blog repo to have something to work with.

This article assumes that the reader has a basic understanding of javascript and Phoenix hooks.

Contents

  1. Generating and appending the button

  2. Building a function responsible for the logic

  3. Adding createAndAddCopyButton to the hook list

Generating and appending the button

First off, we’ll start by creating a simple function that will live in /assets/js/app.js file. Lets call it createAndAddCopyButton, we can position it at the bottom of the app.js. createAndAddCopyButton function will take a pre element as an argument, select all the necessary nodes, create the button and a div element(to help with the positioning of our button), add styles, rearrange html nodes in the DOM and attach an event listener to the button.

const createAndAddCopyButton = (element) => {
  const code = element.querySelector("code")
  const article = document.querySelector("article")
  const button = document.createElement("button")
  const div = document.createElement("div")
  
  div.classList.add("relative")
  button.classList.add("right-2")
  button.classList.add("right-2")
  button.classList.add("text-white")
  button.classList.add("top-2")
  button.classList.add("h-8")
  button.classList.add("w-16")
  button.classList.add("bg-violet-700")
  button.classList.add("rounded-md")
  button.classList.add("font-semibold")
  button.classList.add("hover:bg-violet-500")
  button.classList.add("transition")
  button.classList.add("absolute")
  button.title = "Copy bellow code"
  button.innerHTML = "Copy"

  div.append(button)
  article.insertBefore(div, element)
  element.remove()
  div.append(element)
  button.addEventListener("click", (event) => copyFunction(event, code))
}

Building a function responsible for the logic

Before we tackle the logic, let’s throw in some extra flavour in a form of some simple transitions. We’ll put together a function that will receive an event object and change the text along with some classes. After a second the styles and text will return to its previous values.


const notifyAndCleanUp = (event) => {
  setTimeout(()=>{
    event.target.classList.remove("scale-110")
    event.target.innerHTML = "Copy"
  }, 1000)
  event.target.classList.add("scale-110")
  event.target.innerHTML = "Copied!"
}

As aforementioned, we’ll use browser API. Just under the createAndAddCopyButton let’s create a callback called copyFunction for the event listener we created earlier. It will take an event object and a code element we select earlier. The function will check whether the browser is supporting the API and copy the code node’s innerHTML to the user clipboard if it does. If for what ever reason this check fails, we’ll communicate to the user that their browser doesn’t support this functionality.

const copyFunction = (event, code) => {
  if("clipboard" in navigator){
      navigator.clipboard.writeText(code.innerHTML) 
      notifyAndCleanUp(event)
  } else {
      alert("Sorry, your browser does not support clipboard copy")
  }
}

Adding createAndAddCopyButton to the hook list

Now we’ll create a Hooks object and add our function to it. Let’s position the object above the liveSocket initialisation. Next, we’ll add a new property to the Hooks called HandleCopy. In the life-cycle callback mounted(which means it will fire after the liveview server has finished mounting), we’ll select and loop over all the pre elements calling createAndAddCopyButton, thus creating our button for every pre tag.

let Hooks = {}

Hooks.HandleCopy = {
    mounted() {
      const pre = document.querySelectorAll("pre")
      pre.forEach(p => {
        createAndAddCopyButton(p)
      })
    }
}

With that behind us, it’s time to pass the Hooks object as option to the LiveSocket constructor…

let liveSocket = new LiveSocket("/live", Socket, {
  hooks: Hooks,
  longPollFallbackMs: 2500,
  params: {_csrf_token: csrfToken}
})

…and phx-hook="HandleCopy" as an attribute to a html element that holds our parsed markup. Hooks require an id to be present on the element, so let’s add one.

    <article id="article" phx-hook="HandleCopy" class="prose prose-h1:text-sky-700">
      <%= Phoenix.HTML.raw(Earmark.as_html!(@article)) %>
    </article>

Here’s how the result of our endevour looks like:

image

Well done! After clicking the button, contents of the code element will be copied over to the user’s clipboard.

Additional content:

0 comments