import { Message, useLocale } from '@vuepress/helper/client'; import { useClipboard, useEventListener, useMediaQuery, watchImmediate, } from '@vueuse/core'; import { computed, nextTick } from 'vue'; import { onContentUpdated } from 'vuepress/client'; import '@vuepress/helper/message.css'; import '../styles/copy-code.css'; import '../styles/vars.css'; const CHECK_ICON = ''; const SHELL_RE = /language-(shellscript|shell|bash|sh|zsh)/; /** * Use copy code function * * 使用复制代码功能 * * @example * ```ts * // .vuepress/client.ts * import { useCopyCode } from '@vuepress/plugin-copy-code/client' * * export default { * setup() { * useCopyCode({ * selector: '.custom-code', * duration: 3000, * showInMobile: true * }) * } * } * ``` */ export const useCopyCode = ({ selector, ignoreSelector, inlineSelector, duration = 2000, locales, showInMobile, transform, }) => { if (__VUEPRESS_SSR__) return; /** * On small-screen devices, the copy button is not displayed by default in order to prevent * it from obstructing content, as the `:hover` effect can be triggered by `touch` events. */ const isMobile = useMediaQuery('(max-width: 419px)'); const enabled = computed(() => !isMobile.value || showInMobile); const locale = useLocale(locales); const insertCopyButton = (codeBlockElement) => { if (codeBlockElement.hasAttribute('copy-code')) return; const copyElement = document.createElement('button'); copyElement.type = 'button'; copyElement.classList.add('vp-copy-code-button'); copyElement.setAttribute('aria-label', locale.value.copy); copyElement.setAttribute('data-copied', locale.value.copied); codeBlockElement.parentElement?.insertBefore(copyElement, codeBlockElement); codeBlockElement.setAttribute('copy-code', ''); }; const appendCopyButton = () => { document.body.classList.toggle('no-copy-code', !enabled.value); if (!enabled.value) return; document.querySelectorAll(selector).forEach(insertCopyButton); }; watchImmediate(enabled, () => nextTick(appendCopyButton), { flush: 'post', }); onContentUpdated((reason) => { if (reason !== 'beforeUnmount') appendCopyButton(); }); const { copy } = useClipboard({ legacy: true }); const timeoutIdMap = new WeakMap(); let message = null; const copyContent = async (codeContainer, codeContent, button) => { const clone = codeContent.cloneNode(true); if (ignoreSelector) { clone.querySelectorAll(ignoreSelector).forEach((node) => { node.remove(); }); } if (transform) transform(clone); let text = clone.textContent || ''; if (SHELL_RE.test(codeContainer.className)) text = text.replace(/^ *(\$|>) /gm, ''); await copy(text); if (duration <= 0) return; button.classList.add('copied'); clearTimeout(timeoutIdMap.get(button)); const timeoutId = setTimeout(() => { button.classList.remove('copied'); button.blur(); timeoutIdMap.delete(button); }, duration); timeoutIdMap.set(button, timeoutId); }; useEventListener('click', (event) => { const el = event.target; if (enabled.value && el.matches('div[class*="language-"] > button.vp-copy-code-button')) { const codeContainer = el.parentElement; const preBlock = el.nextElementSibling; if (!codeContainer || !preBlock) return; void copyContent(codeContainer, preBlock, el); } }, { passive: true }); if (inlineSelector) { useEventListener('dblclick', (event) => { const el = event.target; if (enabled.value && el.matches(inlineSelector)) { const selection = window.getSelection(); if (selection && (el.contains(selection.anchorNode) || el.contains(selection.focusNode))) { selection.removeAllRanges(); } void copy(el.textContent || ''); (message ??= new Message()).pop(`${CHECK_ICON}${locale.value.copied} `, duration); } }, { passive: true }); } };