perf: 调整代码复制逻辑,适配 shikiji diff

This commit is contained in:
pengzhanbo 2023-12-28 19:27:09 +08:00
parent bd0f857e6a
commit 90465d2b02
6 changed files with 150 additions and 93 deletions

View File

@ -1,5 +1,5 @@
import { defineClientConfig } from '@vuepress/client'
import { setupCopyCode } from './composables/index.js'
import { setupCopyCode } from './setupCopyCode.js'
import './styles/button.scss'

View File

@ -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)
}
}

View File

@ -1 +0,0 @@
export * from './setup.js'

View File

@ -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<HTMLElement>(selector).forEach(insertBtn)
}
else if (Array.isArray(selector)) {
selector.forEach((item) => {
document.querySelectorAll<HTMLElement>(item).forEach(insertBtn)
})
}
}, delay)
}
onMounted(() => {
if (!isMobile() || options.showInMobile)
generateButton()
})
watch(
() => route.path,
() => {
if (!isMobile() || options.showInMobile)
generateButton()
},
)
}

View File

@ -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<void> {
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<HTMLElement>(item).forEach(insertBtn)
})
}
onMounted(async () => {
if (!isMobile() || options.showInMobile) {
await generateButton()
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-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()
}
}
}

View File

@ -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;
}