Skip to content

Commit 191c789

Browse files
committed
Added a copy button to code lines/blocks
1 parent de1e9eb commit 191c789

File tree

2 files changed

+134
-3
lines changed

2 files changed

+134
-3
lines changed

docs/.vuepress/client.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import {provide} from "vue";
2-
import {defineClientConfig} from "@vuepress/client";
1+
import { provide } from "vue";
2+
import { defineClientConfig } from "@vuepress/client";
33
import mitt from 'mitt';
44

55
import Layout from "./theme/layouts/Layout.vue";
@@ -15,10 +15,12 @@ import social from "./config-client/social";
1515
import Chat from "./components/Chat.vue";
1616
import CodeTabs from "./components/CodeTabs.vue";
1717
import CodeWithCopy from "./components/CodeWithCopy.vue";
18+
import GlobalCopyCode from "./components/GlobalCopyCode.vue";
1819

1920
export default defineClientConfig({
2021
rootComponents: [
2122
Chat,
23+
GlobalCopyCode,
2224
],
2325
async enhance({ app }) {
2426
app.config.globalProperties.$eventBus = mitt();
@@ -69,7 +71,7 @@ export default defineClientConfig({
6971
productsList: ['CloudLinux', 'Imunify', 'TuxCare'],
7072
productsTitle: 'Products',
7173
productsURLs: ['https://docs.cloudlinux.com', 'https://docs.imunify360.com', 'https://docs.tuxcare.com'],
72-
74+
7375
//social links for footer
7476
social,
7577

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
<template>
2+
<div class="user-global-copy-code-root"></div>
3+
</template>
4+
5+
<script setup>
6+
import { onMounted, watch, nextTick } from 'vue';
7+
import { useRoute } from 'vue-router';
8+
9+
const route = useRoute();
10+
11+
const addCopyButtons = () => {
12+
// Select all code blocks. VuePress code blocks typically have class starting with "language-"
13+
const blocks = document.querySelectorAll('div[class*="language-"]');
14+
15+
blocks.forEach(block => {
16+
// Prevent adding multiple buttons
17+
if (block.querySelector('.copy-code-button')) return;
18+
19+
const button = document.createElement('button');
20+
button.className = 'copy-code-button';
21+
button.type = 'button';
22+
button.ariaLabel = 'Copy code';
23+
button.innerHTML = `
24+
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16" class="copy-icon">
25+
<path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"></path>
26+
<path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"></path>
27+
</svg>
28+
<span class="copy-success-icon" style="display: none;">
29+
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16">
30+
<path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z"></path>
31+
</svg>
32+
</span>
33+
`;
34+
35+
button.onclick = async () => {
36+
const pre = block.querySelector('pre');
37+
if (!pre) return;
38+
39+
// Get text content but careful not to include potential line numbers if they are implemented via psuedo elements it's fine.
40+
// If line numbers are separate elements, we might scrape them.
41+
// Standard VuePress prismjs plugin usage:
42+
const code = pre.textContent;
43+
44+
try {
45+
await navigator.clipboard.writeText(code);
46+
47+
// Show success state
48+
const copyIcon = button.querySelector('.copy-icon');
49+
const successIcon = button.querySelector('.copy-success-icon');
50+
51+
if (copyIcon && successIcon) {
52+
copyIcon.style.display = 'none';
53+
successIcon.style.display = 'inline';
54+
button.classList.add('copied');
55+
56+
setTimeout(() => {
57+
copyIcon.style.display = 'inline';
58+
successIcon.style.display = 'none';
59+
button.classList.remove('copied');
60+
}, 2000);
61+
}
62+
} catch (err) {
63+
console.error('Failed to copy!', err);
64+
}
65+
}
66+
67+
// Append to the block
68+
block.appendChild(button);
69+
});
70+
};
71+
72+
onMounted(() => {
73+
// Initial run
74+
addCopyButtons();
75+
76+
// Observer for dynamic changes (if any)
77+
const observer = new MutationObserver(() => {
78+
addCopyButtons();
79+
});
80+
observer.observe(document.body, { childList: true, subtree: true });
81+
});
82+
83+
watch(
84+
() => route.path,
85+
() => {
86+
nextTick(() => {
87+
// Small delay to ensure content is swapped
88+
setTimeout(addCopyButtons, 300);
89+
});
90+
}
91+
);
92+
</script>
93+
94+
<style>
95+
/* Ensure the code block container is relative so absolute button works */
96+
div[class*="language-"] {
97+
position: relative;
98+
}
99+
100+
/* Button styles */
101+
.copy-code-button {
102+
position: absolute;
103+
top: 7px;
104+
right: 10px;
105+
z-index: 10;
106+
padding: 6px;
107+
border: 1px solid rgba(255,255,255,0.1);
108+
background: transparent;
109+
border-radius: 4px;
110+
cursor: pointer;
111+
color: rgba(255,255,255,0.4);
112+
transition: all 0.25s ease;
113+
line-height: 0;
114+
display: flex;
115+
align-items: center;
116+
justify-content: center;
117+
}
118+
119+
.copy-code-button:hover {
120+
background: rgba(255, 255, 255, 0.1);
121+
color: #fff;
122+
border-color: rgba(255,255,255,0.3);
123+
}
124+
125+
.copy-code-button.copied {
126+
color: #4caf50;
127+
border-color: #4caf50;
128+
}
129+
</style>

0 commit comments

Comments
 (0)