diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-08-04 15:39:18 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-08-04 15:39:57 +0300 |
| commit | 4cf7cff3a0f9a4b593f6cb189f0c1cc4378726f0 (patch) | |
| tree | c42f339c4cc99307d40d44b05676592425f41013 /website/src | |
| parent | e6fa5289f2de03bc7babf2ec07efef6b45ed398f (diff) | |
fix(website): copy heading links to clipboard
Diffstat (limited to 'website/src')
| -rw-r--r-- | website/src/components/Head.astro | 57 | ||||
| -rw-r--r-- | website/src/styles/starlight.css | 32 |
2 files changed, 89 insertions, 0 deletions
diff --git a/website/src/components/Head.astro b/website/src/components/Head.astro index 9987173b..dac83b81 100644 --- a/website/src/components/Head.astro +++ b/website/src/components/Head.astro @@ -8,3 +8,60 @@ const version = docsVersion(Astro.url.pathname); {head.map(({ tag: Tag, attrs, content }) => <Tag {...attrs} set:html={content} />)} <meta data-pagefind-filter="version[content]" content={version} /> {version !== 'stable' && <meta name="robots" content="noindex, nofollow" />} + +<script> + const labels = (() => { + const lang = document.documentElement.lang.toLowerCase(); + if (lang.startsWith('ja')) return { copy: 'リンクをコピー', copied: 'コピーしました' }; + if (lang.startsWith('zh')) return { copy: '复制链接', copied: '已复制' }; + return { copy: 'Copy link', copied: 'Copied' }; + })(); + + function labelHeadingLinks(root: ParentNode = document) { + for (const link of root.querySelectorAll<HTMLAnchorElement>('.sl-anchor-link')) { + link.title = labels.copy; + const section = link.querySelector('.sr-only')?.textContent; + if (section) link.setAttribute('aria-label', `${labels.copy}: ${section}`); + } + } + + labelHeadingLinks(); + document.addEventListener('astro:page-load', () => labelHeadingLinks()); + document.addEventListener('click', async (event) => { + if ( + event instanceof MouseEvent && + (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) + ) { + return; + } + + const target = event.target; + if (!(target instanceof Element)) return; + const link = target.closest('.sl-anchor-link'); + if (!(link instanceof HTMLAnchorElement) || !navigator.clipboard?.writeText) return; + + try { + await navigator.clipboard.writeText(link.href); + } catch { + return; + } + + link.querySelector('.heading-link-copy-feedback')?.remove(); + const feedback = document.createElement('span'); + feedback.className = 'heading-link-copy-feedback'; + feedback.setAttribute('aria-hidden', 'true'); + feedback.textContent = labels.copied; + link.append(feedback); + window.setTimeout(() => feedback.remove(), 1400); + + let status = document.querySelector<HTMLElement>('[data-heading-link-copy-status]'); + if (!status) { + status = document.createElement('span'); + status.className = 'sr-only'; + status.setAttribute('role', 'status'); + status.dataset.headingLinkCopyStatus = ''; + document.body.append(status); + } + status.textContent = labels.copied; + }); +</script> diff --git a/website/src/styles/starlight.css b/website/src/styles/starlight.css index c7b82f67..39eef15f 100644 --- a/website/src/styles/starlight.css +++ b/website/src/styles/starlight.css @@ -182,6 +182,38 @@ color: var(--sl-color-accent); } +.sl-markdown-content .heading-link-copy-feedback { + position: absolute; + z-index: 10; + bottom: calc(100% + 0.4rem); + inset-inline-start: 50%; + padding: 0.25rem 0.45rem; + border: 1px solid var(--sl-color-hairline-light); + border-radius: 3px; + background: var(--sl-color-bg-nav); + color: var(--sl-color-white); + font-family: var(--sl-font); + font-size: var(--sl-text-xs); + font-weight: 500; + line-height: 1.2; + white-space: nowrap; + pointer-events: none; + transform: translateX(-50%); + animation: heading-link-copied 1.4s ease both; +} + +@keyframes heading-link-copied { + 0% { opacity: 0; transform: translate(-50%, 0.25rem); } + 15%, 75% { opacity: 1; transform: translate(-50%, 0); } + 100% { opacity: 0; transform: translate(-50%, -0.15rem); } +} + +@media (prefers-reduced-motion: reduce) { + .sl-markdown-content .heading-link-copy-feedback { + animation: none; + } +} + .sl-markdown-content code:not(:where(.not-content *)) { border: 1px solid var(--sl-color-hairline); border-radius: 2px; |
