From 90465d2b02788f095c60e10e0d412d3fad651bc7 Mon Sep 17 00:00:00 2001 From: pengzhanbo Date: Thu, 28 Dec 2023 19:27:09 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20=E8=B0=83=E6=95=B4=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E5=A4=8D=E5=88=B6=E9=80=BB=E8=BE=91=EF=BC=8C=E9=80=82=E9=85=8D?= =?UTF-8?q?=20shikiji=20diff?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/client/clientConfig.ts | 2 +- .../src/client/composables/copyToClipboard.ts | 23 --- .../src/client/composables/index.ts | 1 - .../src/client/composables/setup.ts | 67 -------- .../src/client/setupCopyCode.ts | 147 ++++++++++++++++++ .../src/client/styles/button.scss | 3 +- 6 files changed, 150 insertions(+), 93 deletions(-) delete mode 100644 plugins/plugin-copy-code/src/client/composables/copyToClipboard.ts delete mode 100644 plugins/plugin-copy-code/src/client/composables/index.ts delete mode 100644 plugins/plugin-copy-code/src/client/composables/setup.ts create mode 100644 plugins/plugin-copy-code/src/client/setupCopyCode.ts diff --git a/plugins/plugin-copy-code/src/client/clientConfig.ts b/plugins/plugin-copy-code/src/client/clientConfig.ts index 78240577..06ba938d 100644 --- a/plugins/plugin-copy-code/src/client/clientConfig.ts +++ b/plugins/plugin-copy-code/src/client/clientConfig.ts @@ -1,5 +1,5 @@ import { defineClientConfig } from '@vuepress/client' -import { setupCopyCode } from './composables/index.js' +import { setupCopyCode } from './setupCopyCode.js' import './styles/button.scss' diff --git a/plugins/plugin-copy-code/src/client/composables/copyToClipboard.ts b/plugins/plugin-copy-code/src/client/composables/copyToClipboard.ts deleted file mode 100644 index 8cb0eed1..00000000 --- a/plugins/plugin-copy-code/src/client/composables/copyToClipboard.ts +++ /dev/null @@ -1,23 +0,0 @@ -export function copyToClipboard(str: string): void { - const selection = document.getSelection() - const selectedRange - = selection && selection.rangeCount > 0 ? selection.getRangeAt(0) : false - - const textEl = document.createElement('textarea') - - textEl.value = str - textEl.setAttribute('readonly', '') - textEl.style.position = 'absolute' - textEl.style.top = '-9999px' - document.body.appendChild(textEl) - - textEl.select() - document.execCommand('copy') - - document.body.removeChild(textEl) - - if (selectedRange && selection) { - selection.removeAllRanges() - selection.addRange(selectedRange) - } -} diff --git a/plugins/plugin-copy-code/src/client/composables/index.ts b/plugins/plugin-copy-code/src/client/composables/index.ts deleted file mode 100644 index 94060776..00000000 --- a/plugins/plugin-copy-code/src/client/composables/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './setup.js' diff --git a/plugins/plugin-copy-code/src/client/composables/setup.ts b/plugins/plugin-copy-code/src/client/composables/setup.ts deleted file mode 100644 index 076d25d2..00000000 --- a/plugins/plugin-copy-code/src/client/composables/setup.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { onMounted, watch } from 'vue' -import { useRoute } from 'vue-router' -import type { CopyCodeOptions } from '../../shared/index.js' -import { copyToClipboard } from './copyToClipboard.js' - -declare const __COPY_CODE_OPTIONS__: CopyCodeOptions - -const options = __COPY_CODE_OPTIONS__ - -function isMobile(): boolean { - return navigator - ? /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/iu.test( - navigator.userAgent, - ) - : false -} - -export function setupCopyCode(): void { - const route = useRoute() - - const insertBtn = (codeBlockEl: HTMLElement): void => { - if (codeBlockEl.hasAttribute('has-copy-code')) - return - const button = document.createElement('button') - button.className = 'copy-code-button' - - button.addEventListener('click', () => { - copyToClipboard(codeBlockEl.textContent || '') - button.classList.add('copied') - options.duration - && setTimeout(() => { - button.classList.remove('copied') - }, options.duration) - }) - - if (codeBlockEl.parentElement) - codeBlockEl.parentElement.insertBefore(button, codeBlockEl) - - codeBlockEl.setAttribute('has-copy-code', '') - } - - const generateButton = (): void => { - const { selector, delay } = options - setTimeout(() => { - if (typeof selector === 'string') { - document.querySelectorAll(selector).forEach(insertBtn) - } - else if (Array.isArray(selector)) { - selector.forEach((item) => { - document.querySelectorAll(item).forEach(insertBtn) - }) - } - }, delay) - } - - onMounted(() => { - if (!isMobile() || options.showInMobile) - generateButton() - }) - watch( - () => route.path, - () => { - if (!isMobile() || options.showInMobile) - generateButton() - }, - ) -} diff --git a/plugins/plugin-copy-code/src/client/setupCopyCode.ts b/plugins/plugin-copy-code/src/client/setupCopyCode.ts new file mode 100644 index 00000000..2ca7b951 --- /dev/null +++ b/plugins/plugin-copy-code/src/client/setupCopyCode.ts @@ -0,0 +1,147 @@ +import { nextTick, onMounted, watch } from 'vue' +import { usePageData } from '@vuepress/client' +import type { CopyCodeOptions } from '../shared/index.js' + +declare const __COPY_CODE_OPTIONS__: CopyCodeOptions + +const options = __COPY_CODE_OPTIONS__ +const RE_LANGUAGE = /language-([\w]+)/ +const RE_START_CODE = /^ *(\$|>)/gm +const shells = ['shellscript', 'shell', 'bash', 'sh', 'zsh'] +const ignoredNodes = ['.diff.remove'] + +function isMobile(): boolean { + return navigator + ? /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/iu.test( + navigator.userAgent, + ) + : false +} + +function sleep(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)) +} + +export function setupCopyCode(): void { + const page = usePageData() + + const insertBtn = (codeBlockEl: HTMLElement): void => { + if (codeBlockEl.hasAttribute('has-copy-code')) + return + const button = document.createElement('button') + button.className = 'copy-code-button' + const parent = codeBlockEl.parentElement + + if (parent) { + parent.insertBefore(button, codeBlockEl) + const classes = parent.className + const match = classes.match(RE_LANGUAGE) || [] + if (match[1]) + button.setAttribute('data-lang', match[1]) + } + + codeBlockEl.setAttribute('has-copy-code', '') + } + + const generateButton = async () => { + const { selector, delay } = options + await nextTick() + await sleep(delay || 0) + const selectors = Array.isArray(selector) ? selector : [selector!] + selectors.forEach((item) => { + document.querySelectorAll(item).forEach(insertBtn) + }) + } + + onMounted(async () => { + if (!isMobile() || options.showInMobile) { + await generateButton() + + const timeoutIdMap: WeakMap = new WeakMap() + window.addEventListener('click', (e) => { + const el = e.target as HTMLElement + if (el.matches('div[class*="language-"] > button.copy-code-button')) { + const parent = el.parentElement + const sibling = el.nextElementSibling + if (!parent || !sibling) + return + + // Clone the node and remove the ignored nodes + const clone = sibling.cloneNode(true) as HTMLElement + clone + .querySelectorAll(ignoredNodes.join(',')) + .forEach(node => node.remove()) + + let text = clone.textContent || '' + const lang = el.getAttribute('data-lang') || '' + if (lang && shells.includes(lang)) + text = text.replace(RE_START_CODE, '').trim() + + copyToClipboard(text).then(() => { + el.classList.add('copied') + clearTimeout(timeoutIdMap.get(el)) + const timeoutId = setTimeout(() => { + el.classList.remove('copied') + el.blur() + timeoutIdMap.delete(el) + }, options.duration) + timeoutIdMap.set(el, timeoutId) + }) + } + }) + } + }) + + watch( + () => page.value.path, + () => { + if (!isMobile() || options.showInMobile) + generateButton() + }, + ) +} + +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() + } + } +} diff --git a/plugins/plugin-copy-code/src/client/styles/button.scss b/plugins/plugin-copy-code/src/client/styles/button.scss index 4b6c58a8..8606b9a1 100644 --- a/plugins/plugin-copy-code/src/client/styles/button.scss +++ b/plugins/plugin-copy-code/src/client/styles/button.scss @@ -50,7 +50,8 @@ html[lang='zh-CN'] { } [class*='language-']:hover > .copy-code-button, -[class*='language-'] > .copy-code-button:focus { +[class*='language-'] > .copy-code-button:focus, +[class*='language-'] > .copy-code-button.copied { opacity: 1; }