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
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:
Well done! After clicking the button, contents of the code
element will be copied over to the user’s clipboard.