mirror of
https://github.com/pengzhanbo/vuepress-theme-plume.git
synced 2026-04-23 10:58:13 +08:00
perf: 调整代码复制逻辑,适配 shikiji diff
This commit is contained in:
parent
bd0f857e6a
commit
90465d2b02
@ -1,5 +1,5 @@
|
||||
import { defineClientConfig } from '@vuepress/client'
|
||||
import { setupCopyCode } from './composables/index.js'
|
||||
import { setupCopyCode } from './setupCopyCode.js'
|
||||
|
||||
import './styles/button.scss'
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
export * from './setup.js'
|
||||
@ -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()
|
||||
},
|
||||
)
|
||||
}
|
||||
147
plugins/plugin-copy-code/src/client/setupCopyCode.ts
Normal file
147
plugins/plugin-copy-code/src/client/setupCopyCode.ts
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user