Na alma do post Deixando a pipeline visível para acompanhar deploy do blog, achei que estava deveras complicado citar meus próprios textos. Então, decidi fazer um botão que aparece em uma situação para permitir copiar em markdown o esquema tão logo possível.

O modelo mais ou menos que uso em markdown para citar um post é:

[{{ page.title }}](% post_url {{ page.url | slugify }} %})

Então, se eu tivesse um jeito de passar o título e o slug da página para uma função, eu teria condição de gerar a string acima. Algo como:

function citation2clipboard(title, slug) {
    alert(`[${title}]({% post_url ${slug} %})`)
}

A função de copiar

Para copiar no clipboard, as fontes que eu tinha falavam para usar a API Clipboard. Ao brincar com ela no console, qual minha surpresa ao ler esta mensagem:

Uncaught (in promise) DOMException: Clipboard write was blocked due to lack of user activation.
    <anonymous> debugger eval code:2
    <anonymous> debugger eval code:3

Isso significa que foi sucesso chamar a API, mas eu precisaria de mais robustez para poder usar. Então deixei ligada a uma função de onClick e pronto, foi possível. Vale lembrar que tanto a leitura do que já está no clipboard como a opção de escrever no clipboard são assíncronas.

Ficou assim o código:

async function citation2clipboard(title, slug) {
    try {
        await navigator.clipboard.writeText(`[${title}]({% post_url ${slug} %})`)
        console.log("citação copiada")
    } catch (e) {
        console.log("erro ocorreu")
    }
}

O mecanismo de habilitar

Eu escolhi para habilitar o botão o localStorage. No caso, se o valor associado a computaria-cite for "true". Coloquei para a página observar quando houver mudanças nessa chave pelo storage de outra aba aberta, a aba em segundo plano irá avaliar isso e irá se adaptar.

Clica nos botões abaixo para inserir/remover o valor no localStorage. A caixa de texto contém o valor atual que está associado a essa chave.

A grosso modo, faze-se um localStorage.getItem("computaria-cite") === "true" e, dependendo do valor dessa comparação, podemos colocar o ícone de copiar como visível (display: block) ou como não-visível (display: none).

Os eventos relativos ao armazenamento possuem a chave storage, então para adicionar um evento de armazenamento basta fazer:

window.addEventListener("storage", event => {
    if (event.key == "computaria-cite" || !event.key) {
        algumaAcao(event.newValue);
    }
});

Note que event.key carrega a chave do armazenamento alterado OU nulo caso o localStorage tenha sido limpo de todas os seus valores: localStorage.clear().

Para os casos em que existe a chave, no caso de event.newValue ser nulo aconteceu a remoção do valor, no caso de event.oldValue ser nulo aconteceu a inserção do valor e caso ambos sejam não nulos aconteceu uma atualização.

Não existe event.value, e eu descobri isso a duras penas. Adicionei o seguinte para verificar as propriedades de event:

console.log({
    value: event.value,
    newValue: event.newValue,
    oldValue: event.oldValue
})

O esqueleto no layout

Tal qual o esquema escolhido em deixando a pipeline visível para acompanhar deploy do blog, escolhi usar o defer em um script. Para habilitar corretamente o funcionamento do script, precisei usar um elemento no DOM de ID citationCopier. É o suficiente para o meu caso, pois a partir daí eu consigo manipular ele de acordo com o que eu preciso.

Usei o ícone disponível em https://uxwing.com/copy-icon/ (livre para uso pessoal e comercial, atribuição não necessária) para servir de “copiar”. Coloquei dentro de /_layout/post.html um botão com o ícone e o script para gerenciar isso, e só:

<script defer src="{{ "/js/citation.js" | prepend: site.baseurl }}">
</script>

<button id="citationCopier" style="display: none;" class="icon" onclick="citation2clipboard('{{ page.title }}', '{{ page.url | slugify }}')">{% include uxwing-copy-icon.svg %}</button>

A interface ficou devendo um bocado, mas o botão ali e a mensagem no console já são o suficiente para mim.

Como eu não quero que citation.js rode o Liquid e faça suas indevidas expansões, eu poderia por tudo dentro de um bloco de raw eu simplesmente não adicionar o frontmatter. Optei por hora na segunda opção.