在 VitePress 生成 Markdown 文档中代码块时,会在最终生成的代码块中添加复制代码的按钮。在检查这个按钮的时候发现页面中的仅仅是一个按钮,并没有添加什么点击事件,在翻阅了 VitePress 的代码之后,发现在 src\client\app\composables\copyCode.ts
文件中有该功能的详细实现。
src\client\app\composables\copyCode.ts
typescriptimport { inBrowser } from 'vitepress' export function useCopyCode() { if (inBrowser) { const timeoutIdMap: WeakMap<HTMLElement, NodeJS.Timeout> = new WeakMap() window.addEventListener('click', (e) => { const el = e.target as HTMLElement if (el.matches('div[class*="language-"] > button.copy')) { const parent = el.parentElement const sibling = el.nextElementSibling?.nextElementSibling if (!parent || !sibling) { return } const isShell = /language-(shellscript|shell|bash|sh|zsh)/.test( parent.className ) let text = '' sibling .querySelectorAll('span.line:not(.diff.remove)') .forEach((node) => (text += (node.textContent || '') + '\n')) text = text.slice(0, -1) if (isShell) { text = text.replace(/^ *(\$|>) /gm, '').trim() } copyToClipboard(text).then(() => { el.classList.add('copied') clearTimeout(timeoutIdMap.get(el)) const timeoutId = setTimeout(() => { el.classList.remove('copied') el.blur() timeoutIdMap.delete(el) }, 2000) timeoutIdMap.set(el, timeoutId) }) } }) } } async function copyToClipboard(text: string) { try { return navigator.clipboard.writeText(text) } catch { const element = document.createElement('textarea') const previouslyFocusedElement = document.activeElement element.value = text // Prevent keyboard from showing on mobile element.setAttribute('readonly', '') element.style.contain = 'strict' element.style.position = 'absolute' element.style.left = '-9999px' element.style.fontSize = '12pt' // Prevent zooming on iOS const selection = document.getSelection() const originalRange = selection ? selection.rangeCount > 0 && selection.getRangeAt(0) : null document.body.appendChild(element) element.select() // Explicit selection workaround for iOS element.selectionStart = 0 element.selectionEnd = text.length document.execCommand('copy') document.body.removeChild(element) if (originalRange) { selection!.removeAllRanges() // originalRange can't be truthy when selection is falsy selection!.addRange(originalRange) } // Get the focus back on the previously focused element, if any if (previouslyFocusedElement) { ;(previouslyFocusedElement as HTMLElement).focus() } } }
那么问题来了,如何把这个功能拿来自己使用?
仔细阅读一下源码,首先 useCopyCode
对页面中的 click
事件进行了监听,并且判断是否是复制代码的按钮,如果判断正确,就获取父元素和兄弟元素,父元素是用来判断语言类型,而兄弟元素用来获得所有的代码文本。
从 html 元素中获得文本的代码如下,首先获取 span
元素中类为 line
的元素并排除 .diff.remove
并获取元素的文本,如果文本为 null
或 undefined
则替换为空文本,并在每行的末尾添加换行符。
sibling.querySelectorAll('span.line:not(.diff.remove)')
.forEach((node) => (text += (node.textContent || '') + '\n'))
以下这段代码则是去除 shell
代码中的 $
、>
以及文本前后的空行:
text = text.replace(/^ *(\$|>) /gm, '').trim()
以下这段代码是更改复制按钮的样式:
copyToClipboard(text).then(() => {
el.classList.add('copied') // 为按钮添加 `copied` 类
clearTimeout(timeoutIdMap.get(el)) // 清除 weakmap 中的计时器
const timeoutId = setTimeout(() => {
el.classList.remove('copied') // 去除 `copied` 类
el.blur() // 移除元素的聚焦
timeoutIdMap.delete(el)
}, 2000)
timeoutIdMap.set(el, timeoutId)
})
copyToClipboard
则是将文本复制到系统的剪贴板中。
如果想要自己使用,则只需要在 useCopyCode
代码中按照自己的要求修改即可。
对于复制按钮的 css 样式如下所示:
button.copy{
--size: 28px;
height: var(--size);
width: var(--size);
background: none;
background-image: url('Base64 编码图像,此处省略');
background-repeat: no-repeat;
border: 1px solid #000;
border-radius: 2px;
background-position: 50%;
background-size: 20px;
padding: 5px;
box-sizing: border-box;
}
button.copied{
background-image: url('Base64 编码图像,此处省略');
}