mirror of
https://github.com/pengzhanbo/vuepress-theme-plume.git
synced 2026-04-23 10:58:13 +08:00
feat(plugin-copy-code): 新增一键复制代码插件
This commit is contained in:
parent
1c375f3a39
commit
c2852610dd
21
packages/plugin-copy-code/LICENSE
Normal file
21
packages/plugin-copy-code/LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (C) 2021 - PRESENT by pengzhanbo
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
17
packages/plugin-copy-code/README.md
Normal file
17
packages/plugin-copy-code/README.md
Normal file
@ -0,0 +1,17 @@
|
||||
# `@vuepress-plume/vuepress-plugin-copy-code`
|
||||
|
||||
## Install
|
||||
```
|
||||
yarn add @vuepress-plume/vuepress-plugin-copy-code
|
||||
```
|
||||
## Usage
|
||||
``` js
|
||||
// .vuepress/config.js
|
||||
module.exports = {
|
||||
//...
|
||||
plugins: [
|
||||
copyCodePlugin()
|
||||
]
|
||||
// ...
|
||||
}
|
||||
```
|
||||
45
packages/plugin-copy-code/package.json
Normal file
45
packages/plugin-copy-code/package.json
Normal file
@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "@vuepress-plume/vuepress-plugin-copy-code",
|
||||
"version": "1.0.0-beta.26",
|
||||
"description": "The Plugin for VuePres 2",
|
||||
"homepage": "https://github.com/pengzhanbo/vuepress-theme-plume#readme",
|
||||
"bugs": {
|
||||
"url": "https://github.com/pengzhanbo/vuepress-theme-plume/issues"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/pengzhanbo/vuepress-theme-plume.git"
|
||||
},
|
||||
"license": "MIT",
|
||||
"author": "pengzhanbo <volodymyr@foxmail.com>",
|
||||
"main": "lib/node/index.js",
|
||||
"files": [
|
||||
"lib"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "pnpm run clean && pnpm run copy && pnpm run ts",
|
||||
"clean": "rimraf lib *.tsbuildinfo",
|
||||
"copy": "cpx \"src/**/*.{d.ts,vue,css,scss,jpg,png}\" lib",
|
||||
"copy:watch": "cpx \"src/**/*.{d.ts,vue,css,scss,jpg,png}\" lib -w",
|
||||
"dev": "concurrently \"pnpm copy:watch\" \"pnpm ts:watch\"",
|
||||
"ts": "tsc -b tsconfig.build.json",
|
||||
"ts:watch": "tsc -b tsconfig.build.json --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vuepress/client": "2.0.0-beta.43",
|
||||
"@vuepress/core": "2.0.0-beta.43",
|
||||
"@vuepress/shared": "2.0.0-beta.43",
|
||||
"@vuepress/utils": "2.0.0-beta.43",
|
||||
"vue": "^3.2.33",
|
||||
"vue-router": "^4.0.14"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"keyword": [
|
||||
"VuePress",
|
||||
"vuepress plugin",
|
||||
"copyCode",
|
||||
"vuepress-plugin-plugin-copy-code"
|
||||
]
|
||||
}
|
||||
79
packages/plugin-copy-code/src/client/clientAppSetup.ts
Normal file
79
packages/plugin-copy-code/src/client/clientAppSetup.ts
Normal file
@ -0,0 +1,79 @@
|
||||
import { defineClientAppSetup, useRouteLocale } from '@vuepress/client'
|
||||
import { computed, onMounted, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import type { CopyCodeLocaleOption, CopyCodeOptions } from '../shared'
|
||||
import { copyToClipboard } from './copyToClipboard'
|
||||
import { successSVG } from './svg'
|
||||
|
||||
import './styles/button.scss'
|
||||
|
||||
declare const __COPY_CODE_OPTIONS__: CopyCodeOptions
|
||||
declare const __COPY_CODE_LOCALES_OPTIONS__: CopyCodeLocaleOption
|
||||
|
||||
const options = __COPY_CODE_OPTIONS__
|
||||
const localesOptions = __COPY_CODE_LOCALES_OPTIONS__
|
||||
|
||||
const isMobile = (): boolean =>
|
||||
navigator
|
||||
? /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/iu.test(
|
||||
navigator.userAgent
|
||||
)
|
||||
: false
|
||||
|
||||
export default defineClientAppSetup(() => {
|
||||
const route = useRoute()
|
||||
const lang = useRouteLocale()
|
||||
|
||||
const locale = computed(() => {
|
||||
return localesOptions[lang.value] || localesOptions['/zh/']
|
||||
})
|
||||
|
||||
const insertBtn = (codeBlockEl: HTMLElement): void => {
|
||||
if (codeBlockEl.hasAttribute('has-copy-code')) return
|
||||
const button = document.createElement('button')
|
||||
button.className = 'copy-code-button'
|
||||
button.innerText = locale.value.hint as string
|
||||
|
||||
button.addEventListener('click', () => {
|
||||
copyToClipboard(codeBlockEl.innerText)
|
||||
button.innerHTML = successSVG + (locale.value.copy as string)
|
||||
options.duration &&
|
||||
setTimeout(() => {
|
||||
button.innerText = locale.value.hint as string
|
||||
}, 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()
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
23
packages/plugin-copy-code/src/client/copyToClipboard.ts
Normal file
23
packages/plugin-copy-code/src/client/copyToClipboard.ts
Normal file
@ -0,0 +1,23 @@
|
||||
export const 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)
|
||||
}
|
||||
}
|
||||
31
packages/plugin-copy-code/src/client/styles/button.scss
Normal file
31
packages/plugin-copy-code/src/client/styles/button.scss
Normal file
@ -0,0 +1,31 @@
|
||||
.copy-code-button {
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
outline: 0;
|
||||
border-radius: 4px;
|
||||
padding: 5px 10px;
|
||||
background-color: var(--c-bg-light);
|
||||
color: var(--c-brand);
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
|
||||
div[class*='language-'] & {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 99;
|
||||
opacity: 0;
|
||||
transition: opacity var(--t-color);
|
||||
}
|
||||
|
||||
div[class*='language-']:hover & {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.icon-success {
|
||||
display: inline-block;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
2
packages/plugin-copy-code/src/client/svg.ts
Normal file
2
packages/plugin-copy-code/src/client/svg.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export const successSVG =
|
||||
'<svg t="1651745829084" class="icon-success" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2061" width="200" height="200"><path d="M469.333333 640l0.384 0.384L469.333333 640z m-106.282666 0l-0.384 0.384 0.384-0.384z m48.512 106.666667a87.466667 87.466667 0 0 1-61.653334-24.874667l-179.52-173.632a67.797333 67.797333 0 0 1 0-98.24c28.032-27.157333 73.493333-27.157333 101.589334 0l139.584 134.997333 319.168-308.544c28.032-27.157333 73.493333-27.157333 101.589333 0a67.925333 67.925333 0 0 1 0 98.24L472.981333 722.069333A87.530667 87.530667 0 0 1 411.562667 746.666667z" fill="#78C326" p-id="2062"></path></svg>'
|
||||
6
packages/plugin-copy-code/src/node/index.ts
Normal file
6
packages/plugin-copy-code/src/node/index.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { copyCodePlugin } from './plugin'
|
||||
|
||||
export * from './plugin'
|
||||
export * from '../shared'
|
||||
|
||||
export default copyCodePlugin
|
||||
40
packages/plugin-copy-code/src/node/plugin.ts
Normal file
40
packages/plugin-copy-code/src/node/plugin.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import type { Plugin } from '@vuepress/core'
|
||||
import { path } from '@vuepress/utils'
|
||||
import type { CopyCodeLocaleOption, CopyCodeOptions } from '../shared'
|
||||
|
||||
const defaultOptions: CopyCodeOptions = {
|
||||
selector: '.theme-default-content div[class*="language-"] pre',
|
||||
duration: 1500,
|
||||
delay: 500,
|
||||
showInMobile: false,
|
||||
}
|
||||
|
||||
const defaultLocalesOption: CopyCodeLocaleOption = {
|
||||
'/zh/': {
|
||||
hint: '复制成功',
|
||||
copy: '复制代码',
|
||||
},
|
||||
'/en/': {
|
||||
hint: 'Copied successfully',
|
||||
copy: 'Copy code',
|
||||
},
|
||||
}
|
||||
|
||||
export const copyCodePlugin = (options: CopyCodeOptions): Plugin => {
|
||||
const locales = options.locales || {}
|
||||
delete options.locales
|
||||
|
||||
options = Object.assign({}, defaultOptions, options)
|
||||
const localesOption = Object.assign({}, defaultLocalesOption, locales)
|
||||
|
||||
return {
|
||||
name: '@vuepress-plume/vuepress-plugin-copy-code',
|
||||
|
||||
define: (): Record<string, unknown> => ({
|
||||
__COPY_CODE_OPTIONS__: options,
|
||||
__COPY_CODE_LOCALES_OPTIONS__: localesOption,
|
||||
}),
|
||||
|
||||
clientAppSetupFiles: path.resolve(__dirname, '../client/clientAppSetup.js'),
|
||||
}
|
||||
}
|
||||
46
packages/plugin-copy-code/src/shared/index.ts
Normal file
46
packages/plugin-copy-code/src/shared/index.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import type { LocaleConfig } from '@vuepress/core'
|
||||
export interface CopyCodeOptions {
|
||||
/**
|
||||
* 代码块选择器
|
||||
*
|
||||
* @default '.theme-default-content dev[class*="language-"] pre'
|
||||
*/
|
||||
selector?: string | string[]
|
||||
|
||||
/**
|
||||
* 提示消息显示时间
|
||||
*
|
||||
* @description 设置为 `0` 将会禁用提示
|
||||
*
|
||||
* @default 1500
|
||||
*/
|
||||
duration?: number
|
||||
|
||||
/**
|
||||
* 是否展示在移动端
|
||||
*/
|
||||
showInMobile?: boolean
|
||||
|
||||
/**
|
||||
* 注册复制按钮的延时,单位 ms
|
||||
*
|
||||
* @default 500
|
||||
*/
|
||||
delay?: number
|
||||
|
||||
locales?: CopyCodeLocaleOption
|
||||
}
|
||||
|
||||
export type CopyCodeLocaleOption = LocaleConfig<CopyCodeLocaleData>
|
||||
|
||||
export interface CopyCodeLocaleData {
|
||||
/**
|
||||
* 复制按钮文字
|
||||
*/
|
||||
copy: string
|
||||
|
||||
/**
|
||||
* 复制成功提示文字
|
||||
*/
|
||||
hint: string
|
||||
}
|
||||
12
packages/plugin-copy-code/tsconfig.build.json
Normal file
12
packages/plugin-copy-code/tsconfig.build.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.esm.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.cjs.json"
|
||||
}
|
||||
],
|
||||
"files": []
|
||||
}
|
||||
9
packages/plugin-copy-code/tsconfig.cjs.json
Normal file
9
packages/plugin-copy-code/tsconfig.cjs.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"module": "CommonJS",
|
||||
"rootDir": "./src",
|
||||
"outDir": "./lib"
|
||||
},
|
||||
"include": ["./src/node", "./src/shared"]
|
||||
}
|
||||
10
packages/plugin-copy-code/tsconfig.esm.json
Normal file
10
packages/plugin-copy-code/tsconfig.esm.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"module": "ES2020",
|
||||
"rootDir": "./src",
|
||||
"outDir": "./lib",
|
||||
"types": ["@vuepress/client/types"]
|
||||
},
|
||||
"include": ["./src/client", "./src/shared"]
|
||||
}
|
||||
@ -29,6 +29,7 @@
|
||||
"dependencies": {
|
||||
"@types/lodash.merge": "^4.6.6",
|
||||
"@vuepress-plume/vuepress-plugin-caniuse": "workspace:*",
|
||||
"@vuepress-plume/vuepress-plugin-copy-code": "workspace:*",
|
||||
"@vuepress/client": "2.0.0-beta.43",
|
||||
"@vuepress/core": "2.0.0-beta.43",
|
||||
"@vuepress/plugin-active-header-links": "2.0.0-beta.43",
|
||||
@ -58,10 +59,7 @@
|
||||
"vue": "^3.2.33",
|
||||
"vue-router": "^4.0.14",
|
||||
"vuepress-plugin-comment2": "2.0.0-beta.49",
|
||||
"vuepress-plugin-copy-code2": "2.0.0-beta.49",
|
||||
"vuepress-plugin-md-enhance": "2.0.0-beta.49",
|
||||
"vuepress-plugin-reading-time2": "2.0.0-beta.49",
|
||||
"vuepress-plugin-sass-palette": "2.0.0-beta.49",
|
||||
"vuepress-plugin-seo2": "2.0.0-beta.49",
|
||||
"vuepress-plugin-sitemap2": "2.0.0-beta.49"
|
||||
},
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
// import { copyCodePlugin } from '@vuepress-plume/vuepress-plugin-copy-code'
|
||||
import { copyCodePlugin } from '@vuepress-plume/vuepress-plugin-copy-code'
|
||||
import type { Plugin } from '@vuepress/core'
|
||||
import { copyCodePlugin } from 'vuepress-plugin-copy-code2'
|
||||
import type { PlumeThemePluginOptions } from '../../shared'
|
||||
|
||||
export const resolveCopyCode = (plugins: PlumeThemePluginOptions): Plugin => {
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"references": [
|
||||
{ "path": "../plugin-caniuse/tsconfig.build.json" },
|
||||
{ "path": "../plugin-copy-code/tsconfig.build.json" },
|
||||
{ "path": "./tsconfig.esm.json" },
|
||||
{ "path": "./tsconfig.cjs.json" }
|
||||
],
|
||||
|
||||
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
@ -107,11 +107,17 @@ importers:
|
||||
specifiers:
|
||||
'@vuepress/client': 2.0.0-beta.43
|
||||
'@vuepress/core': 2.0.0-beta.43
|
||||
'@vuepress/shared': 2.0.0-beta.43
|
||||
'@vuepress/utils': 2.0.0-beta.43
|
||||
balloon-css: ^1.2.0
|
||||
vue: ^3.2.33
|
||||
vue-router: ^4.0.14
|
||||
dependencies:
|
||||
'@vuepress/client': 2.0.0-beta.43
|
||||
'@vuepress/core': 2.0.0-beta.43
|
||||
'@vuepress/shared': 2.0.0-beta.43
|
||||
'@vuepress/utils': 2.0.0-beta.43
|
||||
balloon-css: 1.2.0
|
||||
vue: 3.2.33
|
||||
vue-router: 4.0.14_vue@3.2.33
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./packages/theme/tsconfig.build.json" },
|
||||
{ "path": "./packages/plugin-caniuse/tsconfig.build.json" }
|
||||
{ "path": "./packages/plugin-caniuse/tsconfig.build.json" },
|
||||
{ "path": "./packages/plugin-copy-code/tsconfig.build.json" }
|
||||
]
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user