672f997d8b
Injects a hover-revealed copy button into every <pre><code> block
site-wide via a single layout partial, reading textContent so escaped
samples copy as literal text. Ships copy/check icons and matching Sass.
Passes icon markup to JS via <template> elements instead of Twig's
|escape('js'), which calls mb_ord() and fatals without mbstring — same
class of bug as the existing |slice/mb_substr gotcha, now documented in
AGENTS.md.
Closes the "Copy-to-clipboard button on code blocks" backlog item in
novaconium/ISSUES.md.
59 lines
2.5 KiB
Twig
59 lines
2.5 KiB
Twig
{% import '_layout/icons.twig' as icons %}
|
|
{# Adds a hover-revealed copy button to every <pre><code> block on the
|
|
page, without touching any individual doc/blog page's markup. Reads
|
|
textContent (not innerHTML) when copying, so HTML-entity-escaped
|
|
samples (e.g. <h1> in the SEO starter template) come out as their
|
|
literal, unescaped characters rather than the escaped markup.
|
|
|
|
The icon markup is passed to JS via <template> elements (plain HTML
|
|
output, default autoescaping) rather than Twig's `|escape('js')`
|
|
filter — that filter calls Twig\Runtime\mb_ord() under the hood, which
|
|
hard-requires the mbstring extension and fatals
|
|
(`Call to undefined function Twig\Runtime\mb_ord()`) without it, the
|
|
same class of mbstring gotcha documented in AGENTS.md for `|slice` on
|
|
strings. #}
|
|
<template id="copy-code-icon-copy">{{ icons.copy() }}<span class="copy-code-label">Copy</span></template>
|
|
<template id="copy-code-icon-copied">{{ icons.check() }}<span class="copy-code-label">Copied!</span></template>
|
|
<script>
|
|
(function () {
|
|
var copyIconHtml = document.getElementById('copy-code-icon-copy').innerHTML;
|
|
var checkIconHtml = document.getElementById('copy-code-icon-copied').innerHTML;
|
|
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
document.querySelectorAll('pre').forEach(function (pre) {
|
|
if (!pre.querySelector('code')) {
|
|
return;
|
|
}
|
|
|
|
var button = document.createElement('button');
|
|
button.type = 'button';
|
|
button.className = 'copy-code-button icon-link';
|
|
button.setAttribute('aria-label', 'Copy code to clipboard');
|
|
button.innerHTML = copyIconHtml + '<span class="copy-code-label">Copy</span>';
|
|
pre.appendChild(button);
|
|
});
|
|
});
|
|
|
|
document.addEventListener('click', function (event) {
|
|
var button = event.target.closest('.copy-code-button');
|
|
if (!button) {
|
|
return;
|
|
}
|
|
|
|
var code = button.closest('pre').querySelector('code');
|
|
|
|
navigator.clipboard.writeText(code.textContent).then(function () {
|
|
var originalHtml = button.innerHTML;
|
|
|
|
button.innerHTML = checkIconHtml + '<span class="copy-code-label">Copied!</span>';
|
|
button.classList.add('copied');
|
|
|
|
setTimeout(function () {
|
|
button.innerHTML = originalHtml;
|
|
button.classList.remove('copied');
|
|
}, 1500);
|
|
});
|
|
});
|
|
})();
|
|
</script>
|