mirror of
https://github.com/reonokiy/blog.nokiy.net.git
synced 2025-06-15 03:02:53 +02:00
feat: add code copy cutton
This commit is contained in:
parent
9c87c1bf03
commit
3312b30dbf
7 changed files with 132 additions and 5 deletions
|
@ -15,6 +15,7 @@ import { visit } from 'unist-util-visit'
|
|||
import UnoCSS from 'unocss/astro'
|
||||
import { themeConfig } from './src/config'
|
||||
import { langMap } from './src/i18n/config'
|
||||
import { rehypeCodeCopyButton } from './src/plugins/rehype-code-copy-button.mjs'
|
||||
import { rehypeImgToFigure } from './src/plugins/rehype-img-to-figure.mjs'
|
||||
import { rehypeUnwrapImg } from './src/plugins/rehype-unwrap-img.mjs'
|
||||
import { remarkAdmonitions } from './src/plugins/remark-admonitions.mjs'
|
||||
|
@ -78,6 +79,7 @@ export default defineConfig({
|
|||
rehypePlugins: [
|
||||
rehypeKatex,
|
||||
rehypeSlug,
|
||||
rehypeCodeCopyButton,
|
||||
rehypeImgToFigure,
|
||||
rehypeUnwrapImg, // Must be after rehypeImgToFigure
|
||||
[
|
||||
|
|
63
src/components/Widgets/CodeCopyButton.astro
Normal file
63
src/components/Widgets/CodeCopyButton.astro
Normal file
|
@ -0,0 +1,63 @@
|
|||
<script>
|
||||
const copyIcons = {
|
||||
copy: `<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/>
|
||||
</svg>`,
|
||||
success: `<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/>
|
||||
</svg>`,
|
||||
}
|
||||
|
||||
// Store active timeouts to prevent memory leaks
|
||||
const activeTimeouts = new WeakMap<HTMLButtonElement, ReturnType<typeof setTimeout>>()
|
||||
|
||||
async function handleCopy(button: HTMLButtonElement) {
|
||||
const codeElement = button.parentElement?.querySelector('pre code')
|
||||
const code = codeElement?.textContent || ''
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(code)
|
||||
|
||||
// Clear existing timeout to prevent visual glitches on multiple clicks
|
||||
const existingTimeout = activeTimeouts.get(button)
|
||||
if (existingTimeout) {
|
||||
clearTimeout(existingTimeout)
|
||||
}
|
||||
|
||||
button.innerHTML = copyIcons.success
|
||||
|
||||
// Set timeout to revert to copy icon after 2 seconds
|
||||
const timeoutId = setTimeout(() => {
|
||||
button.innerHTML = copyIcons.copy
|
||||
activeTimeouts.delete(button)
|
||||
}, 1000)
|
||||
|
||||
activeTimeouts.set(button, timeoutId)
|
||||
}
|
||||
catch {
|
||||
}
|
||||
}
|
||||
|
||||
function setupCodeCopyButtons() {
|
||||
// Only initialize buttons that haven't been initialized yet
|
||||
document.querySelectorAll<HTMLButtonElement>('.code-copy-button:not([data-initialized])').forEach((button) => {
|
||||
button.innerHTML = copyIcons.copy
|
||||
button.setAttribute('data-initialized', 'true')
|
||||
})
|
||||
}
|
||||
|
||||
// Use event delegation for better performance
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!(e.target instanceof HTMLElement))
|
||||
return
|
||||
|
||||
// Find closest button element if clicked on button or child element
|
||||
const button = e.target.closest('.code-copy-button') as HTMLButtonElement | null
|
||||
if (button) {
|
||||
handleCopy(button)
|
||||
}
|
||||
}, { passive: true })
|
||||
|
||||
setupCodeCopyButtons()
|
||||
document.addEventListener('astro:page-load', setupCodeCopyButtons)
|
||||
</script>
|
|
@ -3,6 +3,7 @@ import Button from '@/components/Button.astro'
|
|||
import Footer from '@/components/Footer.astro'
|
||||
import Header from '@/components/Header.astro'
|
||||
import Navbar from '@/components/Navbar.astro'
|
||||
import CodeCopyButton from '@/components/Widgets/CodeCopyButton.astro'
|
||||
import GithubCard from '@/components/Widgets/GithubCard.astro'
|
||||
import GsapAnimation from '@/components/Widgets/GsapAnimation.astro'
|
||||
import PhotoSwipe from '@/components/Widgets/PhotoSwipe.astro'
|
||||
|
@ -44,7 +45,7 @@ const MarginBottom = isPost && themeConfig.comment?.enabled
|
|||
<div
|
||||
class="mx-auto max-w-205.848 min-h-vh w-full min-h-dvh"
|
||||
p="x-[min(7.25vw,3.731rem)] y-10"
|
||||
lg="mx-[max(5.75rem,calc(50vw-34.25rem))] my-20 max-w-[min(calc(75vw-16rem),44rem)] min-h-full p-0"
|
||||
lg="mx-[max(5rem,calc(50vw-35rem))] my-20 max-w-[min(calc(75vw-16rem),44rem)] min-h-full p-0"
|
||||
>
|
||||
<Header />
|
||||
<Navbar />
|
||||
|
@ -55,7 +56,8 @@ const MarginBottom = isPost && themeConfig.comment?.enabled
|
|||
</div>
|
||||
{showAnimation && <GsapAnimation />}
|
||||
<Button supportedLangs={supportedLangs} />
|
||||
<GithubCard />
|
||||
<CodeCopyButton />
|
||||
<PhotoSwipe />
|
||||
<GithubCard />
|
||||
</body>
|
||||
</html>
|
||||
|
|
37
src/plugins/rehype-code-copy-button.mjs
Normal file
37
src/plugins/rehype-code-copy-button.mjs
Normal file
|
@ -0,0 +1,37 @@
|
|||
import { SKIP, visit } from 'unist-util-visit'
|
||||
|
||||
export function rehypeCodeCopyButton() {
|
||||
return (tree) => {
|
||||
visit(tree, 'element', (node, index, parent) => {
|
||||
if (
|
||||
node.tagName === 'pre'
|
||||
&& node.children?.[0]?.tagName === 'code'
|
||||
&& parent
|
||||
&& !node.properties?.['data-copy-button-added']
|
||||
) {
|
||||
node.properties = node.properties || {}
|
||||
node.properties['data-copy-button-added'] = 'true'
|
||||
|
||||
parent.children[index] = {
|
||||
type: 'element',
|
||||
tagName: 'div',
|
||||
properties: { className: ['code-block-wrapper'] },
|
||||
children: [
|
||||
{
|
||||
type: 'element',
|
||||
tagName: 'button',
|
||||
properties: {
|
||||
'className': ['code-copy-button'],
|
||||
'type': 'button',
|
||||
'aria-label': 'Copy code',
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
node,
|
||||
],
|
||||
}
|
||||
return SKIP
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -139,6 +139,25 @@
|
|||
--at-apply: 'mb-4';
|
||||
}
|
||||
|
||||
/* Code Copy Button */
|
||||
.code-copy-button {
|
||||
--at-apply: 'z-99 absolute top-2.3 right-2.3 w-8 aspect-square uno-round-border border-secondary/15 c-secondary/80 cursor-pointer';
|
||||
--at-apply: 'transition-opacity duration-300 ease-out op-100 bg-background lg:(op-0 bg-background)';
|
||||
}
|
||||
.code-block-wrapper:hover .code-copy-button {
|
||||
--at-apply: 'op-100';
|
||||
}
|
||||
.code-copy-button:hover {
|
||||
--at-apply: 'c-primary/80';
|
||||
}
|
||||
.code-copy-button svg {
|
||||
--at-apply: 'w-4 h-4 block mx-auto';
|
||||
}
|
||||
.code-copy-button svg,
|
||||
.code-copy-button svg path {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* :where(details) {
|
||||
--at-apply: 'my-4 px-4 py-3 border border-solid border-secondary/25 rounded cursor-pointer';
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ html {
|
|||
--at-apply: 'bg-background c-secondary antialiased';
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: oklch(var(--un-preset-theme-colors-secondary) / 0.25) transparent;
|
||||
scrollbar-gutter: stable both-edges;
|
||||
}
|
||||
|
||||
/* Fix Flash Issue On iOS */
|
||||
|
|
|
@ -77,7 +77,7 @@
|
|||
|
||||
/* Code Blocks */
|
||||
.heti :where(pre) {
|
||||
--at-apply: 'my-6 overflow-auto uno-round-border px-4 py-3 bg-secondary/5!';
|
||||
--at-apply: 'overflow-auto uno-round-border px-4 py-3 bg-secondary/5!';
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: oklch(var(--un-preset-theme-colors-secondary) / 0) transparent;
|
||||
transition: scrollbar-color 0.3s ease-out;
|
||||
|
@ -91,10 +91,13 @@
|
|||
html.dark .heti pre span {
|
||||
--at-apply: 'text-[var(--shiki-dark)]!';
|
||||
}
|
||||
.heti :is(h1, h2, h3, h4, h5, h6, pre) + pre {
|
||||
.heti .code-block-wrapper {
|
||||
--at-apply: 'my-6 relative';
|
||||
}
|
||||
.heti :is(h1, h2, h3, h4, h5, h6, .code-block-wrapper) + .code-block-wrapper {
|
||||
--at-apply: 'mt-4';
|
||||
}
|
||||
.heti pre:has(+ pre) {
|
||||
.heti .code-block-wrapper:has(+ .code-block-wrapper) {
|
||||
--at-apply: 'mb-4';
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue