From c2852610dde9bb57ca2b50258eacb661927f70ff Mon Sep 17 00:00:00 2001 From: pengzhanbo Date: Thu, 5 May 2022 18:25:34 +0800 Subject: [PATCH] =?UTF-8?q?feat(plugin-copy-code):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E4=B8=80=E9=94=AE=E5=A4=8D=E5=88=B6=E4=BB=A3=E7=A0=81=E6=8F=92?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/plugin-copy-code/LICENSE | 21 +++++ packages/plugin-copy-code/README.md | 17 ++++ packages/plugin-copy-code/package.json | 45 +++++++++++ .../src/client/clientAppSetup.ts | 79 +++++++++++++++++++ .../src/client/copyToClipboard.ts | 23 ++++++ .../src/client/styles/button.scss | 31 ++++++++ packages/plugin-copy-code/src/client/svg.ts | 2 + packages/plugin-copy-code/src/node/index.ts | 6 ++ packages/plugin-copy-code/src/node/plugin.ts | 40 ++++++++++ packages/plugin-copy-code/src/shared/index.ts | 46 +++++++++++ packages/plugin-copy-code/tsconfig.build.json | 12 +++ packages/plugin-copy-code/tsconfig.cjs.json | 9 +++ packages/plugin-copy-code/tsconfig.esm.json | 10 +++ packages/theme/package.json | 4 +- packages/theme/src/node/plugins/copyCode.ts | 3 +- packages/theme/tsconfig.build.json | 1 + pnpm-lock.yaml | 6 ++ tsconfig.build.json | 3 +- 18 files changed, 352 insertions(+), 6 deletions(-) create mode 100644 packages/plugin-copy-code/LICENSE create mode 100644 packages/plugin-copy-code/README.md create mode 100644 packages/plugin-copy-code/package.json create mode 100644 packages/plugin-copy-code/src/client/clientAppSetup.ts create mode 100644 packages/plugin-copy-code/src/client/copyToClipboard.ts create mode 100644 packages/plugin-copy-code/src/client/styles/button.scss create mode 100644 packages/plugin-copy-code/src/client/svg.ts create mode 100644 packages/plugin-copy-code/src/node/index.ts create mode 100644 packages/plugin-copy-code/src/node/plugin.ts create mode 100644 packages/plugin-copy-code/src/shared/index.ts create mode 100644 packages/plugin-copy-code/tsconfig.build.json create mode 100644 packages/plugin-copy-code/tsconfig.cjs.json create mode 100644 packages/plugin-copy-code/tsconfig.esm.json diff --git a/packages/plugin-copy-code/LICENSE b/packages/plugin-copy-code/LICENSE new file mode 100644 index 00000000..9f677c90 --- /dev/null +++ b/packages/plugin-copy-code/LICENSE @@ -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. diff --git a/packages/plugin-copy-code/README.md b/packages/plugin-copy-code/README.md new file mode 100644 index 00000000..4fe2e2e9 --- /dev/null +++ b/packages/plugin-copy-code/README.md @@ -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() + ] + // ... +} +``` diff --git a/packages/plugin-copy-code/package.json b/packages/plugin-copy-code/package.json new file mode 100644 index 00000000..4a3a73ed --- /dev/null +++ b/packages/plugin-copy-code/package.json @@ -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 ", + "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" + ] +} diff --git a/packages/plugin-copy-code/src/client/clientAppSetup.ts b/packages/plugin-copy-code/src/client/clientAppSetup.ts new file mode 100644 index 00000000..18c1e287 --- /dev/null +++ b/packages/plugin-copy-code/src/client/clientAppSetup.ts @@ -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(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/packages/plugin-copy-code/src/client/copyToClipboard.ts b/packages/plugin-copy-code/src/client/copyToClipboard.ts new file mode 100644 index 00000000..c48ae693 --- /dev/null +++ b/packages/plugin-copy-code/src/client/copyToClipboard.ts @@ -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) + } +} diff --git a/packages/plugin-copy-code/src/client/styles/button.scss b/packages/plugin-copy-code/src/client/styles/button.scss new file mode 100644 index 00000000..41d6f7b1 --- /dev/null +++ b/packages/plugin-copy-code/src/client/styles/button.scss @@ -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; + } +} diff --git a/packages/plugin-copy-code/src/client/svg.ts b/packages/plugin-copy-code/src/client/svg.ts new file mode 100644 index 00000000..5db0e8c8 --- /dev/null +++ b/packages/plugin-copy-code/src/client/svg.ts @@ -0,0 +1,2 @@ +export const successSVG = + '' diff --git a/packages/plugin-copy-code/src/node/index.ts b/packages/plugin-copy-code/src/node/index.ts new file mode 100644 index 00000000..5393e1a6 --- /dev/null +++ b/packages/plugin-copy-code/src/node/index.ts @@ -0,0 +1,6 @@ +import { copyCodePlugin } from './plugin' + +export * from './plugin' +export * from '../shared' + +export default copyCodePlugin diff --git a/packages/plugin-copy-code/src/node/plugin.ts b/packages/plugin-copy-code/src/node/plugin.ts new file mode 100644 index 00000000..f04b00e3 --- /dev/null +++ b/packages/plugin-copy-code/src/node/plugin.ts @@ -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 => ({ + __COPY_CODE_OPTIONS__: options, + __COPY_CODE_LOCALES_OPTIONS__: localesOption, + }), + + clientAppSetupFiles: path.resolve(__dirname, '../client/clientAppSetup.js'), + } +} diff --git a/packages/plugin-copy-code/src/shared/index.ts b/packages/plugin-copy-code/src/shared/index.ts new file mode 100644 index 00000000..3eaaad77 --- /dev/null +++ b/packages/plugin-copy-code/src/shared/index.ts @@ -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 + +export interface CopyCodeLocaleData { + /** + * 复制按钮文字 + */ + copy: string + + /** + * 复制成功提示文字 + */ + hint: string +} diff --git a/packages/plugin-copy-code/tsconfig.build.json b/packages/plugin-copy-code/tsconfig.build.json new file mode 100644 index 00000000..b373aff3 --- /dev/null +++ b/packages/plugin-copy-code/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.base.json", + "references": [ + { + "path": "./tsconfig.esm.json" + }, + { + "path": "./tsconfig.cjs.json" + } + ], + "files": [] +} diff --git a/packages/plugin-copy-code/tsconfig.cjs.json b/packages/plugin-copy-code/tsconfig.cjs.json new file mode 100644 index 00000000..2d998114 --- /dev/null +++ b/packages/plugin-copy-code/tsconfig.cjs.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "CommonJS", + "rootDir": "./src", + "outDir": "./lib" + }, + "include": ["./src/node", "./src/shared"] +} diff --git a/packages/plugin-copy-code/tsconfig.esm.json b/packages/plugin-copy-code/tsconfig.esm.json new file mode 100644 index 00000000..6ddae220 --- /dev/null +++ b/packages/plugin-copy-code/tsconfig.esm.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "ES2020", + "rootDir": "./src", + "outDir": "./lib", + "types": ["@vuepress/client/types"] + }, + "include": ["./src/client", "./src/shared"] +} diff --git a/packages/theme/package.json b/packages/theme/package.json index 22efd67d..cdd31645 100644 --- a/packages/theme/package.json +++ b/packages/theme/package.json @@ -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" }, diff --git a/packages/theme/src/node/plugins/copyCode.ts b/packages/theme/src/node/plugins/copyCode.ts index efdabfb0..3ad89f41 100644 --- a/packages/theme/src/node/plugins/copyCode.ts +++ b/packages/theme/src/node/plugins/copyCode.ts @@ -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 => { diff --git a/packages/theme/tsconfig.build.json b/packages/theme/tsconfig.build.json index 75ed7c2e..722b5717 100644 --- a/packages/theme/tsconfig.build.json +++ b/packages/theme/tsconfig.build.json @@ -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" } ], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dbab2f3e..4edd64da 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/tsconfig.build.json b/tsconfig.build.json index fddd0249..a3f9c019 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -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" } ] }