Copy heading URL to clipboard on anchor click (#2773)

The AnchorJS link icon already updated the address bar; it now also
copies the absolute URL and shows a "Copied" toast at the bottom of
the screen.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Abdon Pijpelink
2026-09-24 10:48:36 +02:00
committed by GitHub
co-authored by Claude Opus 5
parent 0fccb0cac1
commit d43d4196ca
3 changed files with 84 additions and 0 deletions
@@ -113,3 +113,4 @@ body {
@import 'partials/footer';
@import 'partials/not-found';
@import 'partials/go-to-top-button';
@import 'partials/anchor-link';
@@ -0,0 +1,36 @@
// Bottom-of-screen "Copied" confirmation for the heading links added by
// assets/js/vendor/anchor.js. That script creates and toggles the element.
// Dark in both themes: a light chip is too bright against the dark theme.
.anchor-copied-toast {
position: fixed;
left: 50%;
bottom: 28px;
z-index: 1000;
transform: translate(-50%, 8px);
padding: 8px 14px;
border-radius: 4px;
font-size: 14px;
line-height: 1.4;
white-space: nowrap;
color: $neutral-98;
background: $neutral-30;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.28);
opacity: 0;
pointer-events: none;
transition:
opacity 0.15s ease,
transform 0.15s ease;
}
.anchor-copied-toast--visible {
opacity: 1;
transform: translate(-50%, 0);
}
// Respect reduced motion preference
@media (prefers-reduced-motion: reduce) {
.anchor-copied-toast {
transition: none;
transform: translate(-50%, 0);
}
}
@@ -1,12 +1,59 @@
import AnchorJS from 'anchor-js';
const TOAST_CLASS = 'anchor-copied-toast';
const TOAST_VISIBLE_CLASS = 'anchor-copied-toast--visible';
const TOAST_DURATION = 1800;
const anchors = new AnchorJS();
anchors.options.placement = 'left';
anchors.options.class = 'text-decoration-none';
let toast = null;
let toastTimeout = null;
// Kept empty until a copy happens, so the live region announces each one.
function createToast() {
toast = document.createElement('div');
toast.className = TOAST_CLASS;
toast.setAttribute('aria-live', 'polite');
document.body.appendChild(toast);
}
function showToast() {
if (!toast) return;
clearTimeout(toastTimeout);
toast.textContent = 'Copied';
toast.classList.add(TOAST_VISIBLE_CLASS);
toastTimeout = setTimeout(() => {
toast.classList.remove(TOAST_VISIBLE_CLASS);
toast.textContent = '';
}, TOAST_DURATION);
}
document.addEventListener('DOMContentLoaded', function (event) {
anchors.add('.qdrant-post__body :is(h1, h2, h3, h4, h5, h6)');
if (/documentation|articles|course/.test(window.location?.pathname)) {
anchors.add('.documentation-article > :is(h1, h2, h3, h4, h5, h6)');
}
createToast();
});
// Copy the heading URL on click, on top of the address bar update the link
// already does natively. Delegated, because anchors.add() injects the links
// after this script runs.
document.addEventListener('click', async function (event) {
const link = event.target.closest('.anchorjs-link');
if (!link) return;
try {
// link.href is already absolute, and matches what the address bar gets.
await navigator.clipboard.writeText(link.href);
showToast();
} catch (e) {
// Clipboard access needs a secure context; the link still works without it.
console.error(e);
}
});