mirror of
https://github.com/reonokiy/blog.nokiy.net.git
synced 2025-06-15 11:12:54 +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 UnoCSS from 'unocss/astro'
|
||||||
import { themeConfig } from './src/config'
|
import { themeConfig } from './src/config'
|
||||||
import { langMap } from './src/i18n/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 { rehypeImgToFigure } from './src/plugins/rehype-img-to-figure.mjs'
|
||||||
import { rehypeUnwrapImg } from './src/plugins/rehype-unwrap-img.mjs'
|
import { rehypeUnwrapImg } from './src/plugins/rehype-unwrap-img.mjs'
|
||||||
import { remarkAdmonitions } from './src/plugins/remark-admonitions.mjs'
|
import { remarkAdmonitions } from './src/plugins/remark-admonitions.mjs'
|
||||||
|
@ -78,6 +79,7 @@ export default defineConfig({
|
||||||
rehypePlugins: [
|
rehypePlugins: [
|
||||||
rehypeKatex,
|
rehypeKatex,
|
||||||
rehypeSlug,
|
rehypeSlug,
|
||||||
|
rehypeCodeCopyButton,
|
||||||
rehypeImgToFigure,
|
rehypeImgToFigure,
|
||||||
rehypeUnwrapImg, // Must be after 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 Footer from '@/components/Footer.astro'
|
||||||
import Header from '@/components/Header.astro'
|
import Header from '@/components/Header.astro'
|
||||||
import Navbar from '@/components/Navbar.astro'
|
import Navbar from '@/components/Navbar.astro'
|
||||||
|
import CodeCopyButton from '@/components/Widgets/CodeCopyButton.astro'
|
||||||
import GithubCard from '@/components/Widgets/GithubCard.astro'
|
import GithubCard from '@/components/Widgets/GithubCard.astro'
|
||||||
import GsapAnimation from '@/components/Widgets/GsapAnimation.astro'
|
import GsapAnimation from '@/components/Widgets/GsapAnimation.astro'
|
||||||
import PhotoSwipe from '@/components/Widgets/PhotoSwipe.astro'
|
import PhotoSwipe from '@/components/Widgets/PhotoSwipe.astro'
|
||||||
|
@ -44,7 +45,7 @@ const MarginBottom = isPost && themeConfig.comment?.enabled
|
||||||
<div
|
<div
|
||||||
class="mx-auto max-w-205.848 min-h-vh w-full min-h-dvh"
|
class="mx-auto max-w-205.848 min-h-vh w-full min-h-dvh"
|
||||||
p="x-[min(7.25vw,3.731rem)] y-10"
|
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 />
|
<Header />
|
||||||
<Navbar />
|
<Navbar />
|
||||||
|
@ -55,7 +56,8 @@ const MarginBottom = isPost && themeConfig.comment?.enabled
|
||||||
</div>
|
</div>
|
||||||
{showAnimation && <GsapAnimation />}
|
{showAnimation && <GsapAnimation />}
|
||||||
<Button supportedLangs={supportedLangs} />
|
<Button supportedLangs={supportedLangs} />
|
||||||
<GithubCard />
|
<CodeCopyButton />
|
||||||
<PhotoSwipe />
|
<PhotoSwipe />
|
||||||
|
<GithubCard />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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';
|
--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) {
|
/* :where(details) {
|
||||||
--at-apply: 'my-4 px-4 py-3 border border-solid border-secondary/25 rounded cursor-pointer';
|
--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';
|
--at-apply: 'bg-background c-secondary antialiased';
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: oklch(var(--un-preset-theme-colors-secondary) / 0.25) transparent;
|
scrollbar-color: oklch(var(--un-preset-theme-colors-secondary) / 0.25) transparent;
|
||||||
|
scrollbar-gutter: stable both-edges;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Fix Flash Issue On iOS */
|
/* Fix Flash Issue On iOS */
|
||||||
|
|
|
@ -77,7 +77,7 @@
|
||||||
|
|
||||||
/* Code Blocks */
|
/* Code Blocks */
|
||||||
.heti :where(pre) {
|
.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-width: thin;
|
||||||
scrollbar-color: oklch(var(--un-preset-theme-colors-secondary) / 0) transparent;
|
scrollbar-color: oklch(var(--un-preset-theme-colors-secondary) / 0) transparent;
|
||||||
transition: scrollbar-color 0.3s ease-out;
|
transition: scrollbar-color 0.3s ease-out;
|
||||||
|
@ -91,10 +91,13 @@
|
||||||
html.dark .heti pre span {
|
html.dark .heti pre span {
|
||||||
--at-apply: 'text-[var(--shiki-dark)]!';
|
--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';
|
--at-apply: 'mt-4';
|
||||||
}
|
}
|
||||||
.heti pre:has(+ pre) {
|
.heti .code-block-wrapper:has(+ .code-block-wrapper) {
|
||||||
--at-apply: 'mb-4';
|
--at-apply: 'mb-4';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue