diff --git a/.vscode/settings.json b/.vscode/settings.json index ebe86f5e9c..1daa0feb53 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -32,6 +32,7 @@ "mdit", "nord", "nprogress", + "photoswipe", "prefetch", "preload", "prismjs", diff --git a/docs/.vuepress/configs/navbar/en.ts b/docs/.vuepress/configs/navbar/en.ts index 987730089e..8ccae8a4b5 100644 --- a/docs/.vuepress/configs/navbar/en.ts +++ b/docs/.vuepress/configs/navbar/en.ts @@ -29,6 +29,7 @@ export const navbarEn: NavbarConfig = [ '/plugins/google-analytics', '/plugins/medium-zoom', '/plugins/nprogress', + '/plugins/photo-swipe', '/plugins/redirect', '/plugins/register-components', ], diff --git a/docs/.vuepress/configs/navbar/zh.ts b/docs/.vuepress/configs/navbar/zh.ts index 602a9186d9..4727c0b64d 100644 --- a/docs/.vuepress/configs/navbar/zh.ts +++ b/docs/.vuepress/configs/navbar/zh.ts @@ -29,6 +29,7 @@ export const navbarZh: NavbarConfig = [ '/zh/plugins/google-analytics', '/zh/plugins/medium-zoom', '/zh/plugins/nprogress', + '/zh/plugins/photo-swipe', '/zh/plugins/redirect', '/zh/plugins/register-components', ], diff --git a/docs/.vuepress/configs/sidebar/en.ts b/docs/.vuepress/configs/sidebar/en.ts index 0c34c63e11..8e424ca845 100644 --- a/docs/.vuepress/configs/sidebar/en.ts +++ b/docs/.vuepress/configs/sidebar/en.ts @@ -14,6 +14,7 @@ export const sidebarEn: SidebarConfig = { '/plugins/google-analytics', '/plugins/medium-zoom', '/plugins/nprogress', + '/plugins/photo-swipe', '/plugins/redirect', '/plugins/register-components', ], diff --git a/docs/.vuepress/configs/sidebar/zh.ts b/docs/.vuepress/configs/sidebar/zh.ts index 5489a6ff5c..4a71f15f09 100644 --- a/docs/.vuepress/configs/sidebar/zh.ts +++ b/docs/.vuepress/configs/sidebar/zh.ts @@ -14,6 +14,7 @@ export const sidebarZh: SidebarConfig = { '/zh/plugins/google-analytics', '/zh/plugins/medium-zoom', '/zh/plugins/nprogress', + '/zh/plugins/photo-swipe', '/zh/plugins/redirect', '/zh/plugins/register-components', ], diff --git a/docs/package.json b/docs/package.json index 0a4c547631..6cdb02f7dc 100644 --- a/docs/package.json +++ b/docs/package.json @@ -19,6 +19,7 @@ "@vuepress/plugin-google-analytics": "workspace:*", "@vuepress/plugin-medium-zoom": "workspace:*", "@vuepress/plugin-nprogress": "workspace:*", + "@vuepress/plugin-photo-swipe": "workspace:*", "@vuepress/plugin-pwa-popup": "workspace:*", "@vuepress/plugin-redirect": "workspace:*", "@vuepress/plugin-register-components": "workspace:*", diff --git a/docs/plugins/photo-swipe.md b/docs/plugins/photo-swipe.md new file mode 100644 index 0000000000..a0ac80b59f --- /dev/null +++ b/docs/plugins/photo-swipe.md @@ -0,0 +1,271 @@ +# photo-swipe + + + +This plugin will make the pictures in the body of the page enter the preview mode when clicked. + +## Usage + +```bash +npm i -D @vuepress/plugin-photo-swipe@next +``` + +```ts +import { photoSwipePlugin } from '@vuepress/plugin-photo-swipe' + +export default { + plugins: [ + photoSwipePlugin({ + // options + }), + ], +} +``` + +In preview mode, you can: + +- Swipe left and right to preview other pictures on the page in order +- View the description of the picture +- Zoom in and zoom out the picture +- View pictures in full screen +- Download pictures +- Share pictures + +::: tip + +- Besides clicking "×" in the upper right corner to exit the preview mode, scrolling up and down more than a certain distance will also exit preview mode. +- On mobile devices, or using the PC trackpad, you can use pan and zoom gestures to pan and zoom in the preview mode. + +::: + +## Config + +### selector + +- Type: `string | string[]` +- Default: `".theme-default-content :not(a) > img:not([no-view])"` +- Details: Image selector + +### scrollToClose + +- Type: `boolean` +- Default: `true` +- Details: Whether close the current image when scrolling. + +### delay + +- Type: `number` +- Default: `800` +- Details: + + The delay of operating dom, in ms. + + ::: tip + + If the theme you are using has a switching animation, it is recommended to configure this option to `Switch animation duration + 200`. + + ::: + +### locales + +- Type: `PhotoSwipeLocaleConfig` + + ```ts + interface PhotoSwipeLocaleData { + /** + * Close button label text + */ + close: string + + /** + * Full screen button label text + */ + fullscreen: string + + /** + * Share button label text + */ + share: string + + /** + * Zoom button label text + */ + zoom: string + + /** + * Previous image button label text + */ + prev: string + + /** + * Next image button label text + */ + next: string + + /** + * Share button config + */ + buttons: PhotoSwipeDefaultUI.ShareButtonData[] + } + + interface PhotoSwipeLocaleConfig { + [localePath: string]: PhotoSwipeLocaleData + } + ``` + +- Details: Locales config for photo-swipe plugin. + +- Example: + + ```ts + import { defineUserConfig } from 'vuepress' + import { photoSwipePlugin } from '@vuepress/plugin-photo-swipe' + + export default defineUserConfig({ + locales: { + '/': { + // this is a supported language + lang: 'en-US', + }, + '/xx/': { + // the plugin does not support this language + lang: 'mm-NN', + }, + }, + + plugins: [ + photoSwipePlugin({ + locales: { + '/': { + // Override share label text + share: 'Share with friends', + }, + + '/xx/': { + // Complete locale config for `mm-NN` language here + }, + }, + }), + ], + }) + ``` + +::: details Built-in Supported Languages + +- **Simplified Chinese** (zh-CN) +- **Traditional Chinese** (zh-TW) +- **English (United States)** (en-US) +- **German** (de-DE) +- **German (Australia)** (de-AT) +- **Russian** (ru-RU) +- **Ukrainian** (uk-UA) +- **Vietnamese** (vi-VN) +- **Portuguese (Brazil)** (pt-BR) +- **Polish** (pl-PL) +- **French** (fr-FR) +- **Spanish** (es-ES) +- **Slovak** (sk-SK) +- **Japanese** (ja-JP) +- **Turkish** (tr-TR) +- **Korean** (ko-KR) +- **Finnish** (fi-FI) +- **Indonesian** (id-ID) +- **Dutch** (nl-NL) + +::: + +## Frontmatter + +- Type: `string | false` +- Details: + +Image selector for the current page, or `false` to disable photo-swipe in current page. + +## Client Config + +### definePhotoSwipeConfig + +Options passed to [`photo-swipe`](http://photoswipe.com/) + +```ts title=".vuepress/client.ts" +import { definePhotoSwipeConfig } from '@vuepress/plugin-photo-swipe/client' + +definePhotoSwipeConfig({ + // set photoswipe options here +}) + +export default {} +``` + +## API + +You can also call photoswipe with apis. + +`createPhotoSwipe` allows you to programmatically view images links with PhotoSwipe: + +```vue + + + +``` + +`registerPhotoSwipe` allows you to register photoswipe for the given image elements: + +```vue + +``` + +## Styles + +You can customize the style via CSS variables: + +@[code css](@vuepress/plugin-photo-swipe/src/client/styles/vars.css) diff --git a/docs/zh/plugins/photo-swipe.md b/docs/zh/plugins/photo-swipe.md new file mode 100644 index 0000000000..b58169eecf --- /dev/null +++ b/docs/zh/plugins/photo-swipe.md @@ -0,0 +1,273 @@ +# photo-swipe + + + +此插件会使页面正文内的图片在点击时进入浏览模式浏览。 + +## 使用方法 + +```bash +npm i -D @vuepress/plugin-photo-swipe@next +``` + +```ts +import { photoSwipePlugin } from '@vuepress/plugin-photo-swipe' + +export default { + plugins: [ + photoSwipePlugin({ + // 选项 + }), + ], +} +``` + +在图片预览模式中,你可以: + +- 左右滑动按顺序浏览页面内其他的图片 +- 查看图片的描述 +- 对图片进行缩放 +- 全屏浏览图片 +- 下载图片 +- 分享图片 + +::: tip + +- 除了点击右上角的 "×" 退出浏览模式外,在上下滚动超过一定距离后,会自动退出图片浏览模式。 +- 在移动端,或使用 PC 触控板,你可以使用平移、缩放手势在浏览模式中平移、缩放图片。 + +::: + +## 插件选项 + +### selector + +- 类型:`string | string[]` +- 默认值:`".theme-default-content :not(a) > img:not([no-view])"` +- 详情:图片选择器 + +### scrollToClose + +- 类型:`boolean` +- 默认值:`true` +- 详情:是否在滚动时关闭当前图片。 + +### delay + +- 类型:`number` +- 默认值:`800` +- 详情: + + 操作页面 DOM 的延时,单位 ms。 + + ::: tip + + 如果你使用的主题有切换动画,建议配置此选项为 `切换动画时长 + 200`。 + + ::: + +### locales + +- 类型:`PhotoSwipeLocaleConfig` + + ```ts + interface PhotoSwipeLocaleData { + /** + * 关闭按钮标签文字 + */ + close: string + + /** + * 全屏按钮标签文字 + */ + fullscreen: string + + /** + * 分享按钮标签文字 + */ + share: string + + /** + * 缩放按钮标签文字 + */ + zoom: string + + /** + * 上一张图片按钮标签文字 + */ + prev: string + + /** + * 下一张图片按钮标签文字 + */ + next: string + + /** + * 功能按钮配置 + */ + buttons: PhotoSwipeDefaultUI.ShareButtonData[] + } + + interface PhotoSwipeLocaleConfig { + [localePath: string]: PhotoSwipeLocaleData + } + ``` + +- 详情:Photo Swipe 插件的国际化配置。 + +- 示例: + + ```ts + import { defineUserConfig } from 'vuepress' + import { photoSwipePlugin } from '@vuepress/plugin-photo-swipe' + + export default defineUserConfig({ + locales: { + '/': { + // 这是一个支持的语言 + lang: 'zh-CN', + }, + '/xx/': { + // 这是一个没有收到插件支持的语言 + lang: 'mm-NN', + }, + }, + + plugins: [ + photoSwipePlugin({ + locales: { + '/': { + // 覆盖分享标签文字 + share: '分享给伙伴', + }, + + '/xx/': { + // 在这里完整设置 `mm-NN` 的多语言配置 + }, + }, + }), + ], + }) + ``` + +::: details 内置支持语言 + +- **简体中文** (zh-CN) +- **繁体中文** (zh-TW) +- **英文(美国)** (en-US) +- **德语** (de-DE) +- **德语(澳大利亚)** (de-AT) +- **俄语** (ru-RU) +- **乌克兰语** (uk-UA) +- **越南语** (vi-VN) +- **葡萄牙语(巴西)** (pt-BR) +- **波兰语** (pl-PL) +- **法语** (fr-FR) +- **西班牙语** (es-ES) +- **斯洛伐克** (sk-SK) +- **日语** (ja-JP) +- **土耳其语** (tr-TR) +- **韩语** (ko-KR) +- **芬兰语** (fi-FI) +- **印尼语** (id-ID) +- **荷兰语** (nl-NL) + +::: + +## Frontmatter + +### photoswipe + +- 类型: `string | false` +- 详情: + + 当前页面的图片选择器或 `false` 以在当前页面中禁用 photo-swipe。 + +## 客户端配置 + +### definePhotoSwipeConfig + +传递给 [`photo-swipe`](http://photoswipe.com/) 的额外选项。 + +```ts title=".vuepress/client.ts" +import { definePhotoSwipeConfig } from '@vuepress/plugin-photo-swipe/client' + +definePhotoSwipeConfig({ + // 在此设置 photoswipe 选项 +}) + +export default {} +``` + +## API + +你可以通过 API 来调用 photoswipe。 + +`createPhotoSwipe` 允许你以编程的方式查看图片链接: + +```vue + + + +``` + +`registerPhotoSwipe` 允许你为给定的图片元素注册 photoswipe: + +```vue + +``` + +## 样式 + +你可以通过 CSS 变量来自定义部分样式: + +@[code css](@vuepress/plugin-photo-swipe/src/client/styles/vars.css) diff --git a/plugins/plugin-photo-swipe/package.json b/plugins/plugin-photo-swipe/package.json new file mode 100644 index 0000000000..51126d15f5 --- /dev/null +++ b/plugins/plugin-photo-swipe/package.json @@ -0,0 +1,57 @@ +{ + "name": "@vuepress/plugin-photo-swipe", + "version": "2.0.0-rc.11", + "description": "VuePress plugin - photo-swipe", + "keywords": [ + "vuepress-plugin", + "vuepress", + "photo-swipe", + "image", + "preview", + "zoom" + ], + "homepage": "https://ecosystem.vuejs.press/plugins/photo-swipe.html", + "bugs": { + "url": "https://github.com/vuepress/ecosystem/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/vuepress/ecosystem.git", + "directory": "plugins/plugin-photo-swipe" + }, + "license": "MIT", + "author": { + "name": "Mr.Hope", + "email": "mister-hope@outlook.com", + "url": "https://mister-hope.com" + }, + "type": "module", + "exports": { + ".": "./lib/node/index.js", + "./client": "./lib/client/index.js", + "./client/*": "./lib/client/*", + "./package.json": "./package.json" + }, + "main": "./lib/node/index.js", + "types": "./lib/node/index.d.ts", + "files": [ + "lib" + ], + "scripts": { + "build": "tsc -b tsconfig.build.json", + "clean": "rimraf --glob ./lib ./*.tsbuildinfo", + "style": "sass src:lib --no-source-map" + }, + "dependencies": { + "@vuepress/helper": "workspace:~2.0.0-rc.11", + "@vueuse/core": "^10.7.2", + "photoswipe": "^5.4.3", + "vue": "^3.4.15" + }, + "peerDependencies": { + "vuepress": "2.0.0-rc.6" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/plugins/plugin-photo-swipe/src/client/composables/index.ts b/plugins/plugin-photo-swipe/src/client/composables/index.ts new file mode 100644 index 0000000000..ed58be28a1 --- /dev/null +++ b/plugins/plugin-photo-swipe/src/client/composables/index.ts @@ -0,0 +1 @@ +export * from './usePhotoSwipe.js' diff --git a/plugins/plugin-photo-swipe/src/client/composables/usePhotoSwipe.ts b/plugins/plugin-photo-swipe/src/client/composables/usePhotoSwipe.ts new file mode 100644 index 0000000000..1c7f6e0ae3 --- /dev/null +++ b/plugins/plugin-photo-swipe/src/client/composables/usePhotoSwipe.ts @@ -0,0 +1,64 @@ +import { useLocaleConfig } from '@vuepress/helper/client' +import { nextTick, onMounted, onUnmounted, watch } from 'vue' +import { usePageData } from 'vuepress/client' +import type { PhotoSwipePluginLocaleData } from '../../shared/index.js' +import { usePhotoSwipeOptions } from '../helpers/index.js' +import { getImages, registerPhotoSwipe } from '../utils/index.js' + +import 'photoswipe/dist/photoswipe.css' +import '../styles/photo-swipe.scss' + +export interface UsePhotoSwipeOptions { + selector: string | string[] + locales: Record< + string, + Record<`${keyof PhotoSwipePluginLocaleData}Title`, string> + > + /** @default 500 */ + delay?: number + /** @default true */ + scrollToClose?: boolean +} + +export const usePhotoSwipe = ({ + selector, + locales, + delay = 500, + scrollToClose = true, +}: UsePhotoSwipeOptions): void => { + const photoSwipeOptions = usePhotoSwipeOptions() + const locale = useLocaleConfig(locales) + const page = usePageData() + + let destroy: (() => void) | null = null + + const setupPhotoSwipe = (): Promise => + new Promise((resolve) => setTimeout(resolve, delay)) + .then(() => nextTick()) + .then(async () => { + destroy = await registerPhotoSwipe( + getImages(selector), + { + ...photoSwipeOptions, + ...locale.value, + }, + scrollToClose, + ) + }) + + onMounted(() => { + setupPhotoSwipe() + + watch( + () => page.value.path, + () => { + destroy?.() + setupPhotoSwipe() + }, + ) + }) + + onUnmounted(() => { + destroy?.() + }) +} diff --git a/plugins/plugin-photo-swipe/src/client/config.ts b/plugins/plugin-photo-swipe/src/client/config.ts new file mode 100644 index 0000000000..e57ba6a9e2 --- /dev/null +++ b/plugins/plugin-photo-swipe/src/client/config.ts @@ -0,0 +1,35 @@ +import type { ClientConfig } from 'vuepress/client' +import { defineClientConfig } from 'vuepress/client' +import type { PhotoSwipePluginLocaleData } from '../shared/index.js' +import { usePhotoSwipe } from './composables/index.js' +import { injectPhotoSwipeConfig } from './helpers/index.js' + +import './styles/vars.css' + +declare const __PS_SELECTOR__: string | string[] +declare const __PS_DELAY__: number +declare const __PS_LOCALES__: Record< + string, + Record<`${keyof PhotoSwipePluginLocaleData}Title`, string> +> +declare const __PS_SCROLL_TO_CLOSE__: boolean + +const selector = __PS_SELECTOR__ +const locales = __PS_LOCALES__ +const delay = __PS_DELAY__ +const scrollToClose = __PS_SCROLL_TO_CLOSE__ + +// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion +export default defineClientConfig({ + enhance: ({ app }) => { + injectPhotoSwipeConfig(app) + }, + setup: () => { + usePhotoSwipe({ + selector, + delay, + locales, + scrollToClose, + }) + }, +}) as ClientConfig diff --git a/plugins/plugin-photo-swipe/src/client/helpers/index.ts b/plugins/plugin-photo-swipe/src/client/helpers/index.ts new file mode 100644 index 0000000000..db039e3f2b --- /dev/null +++ b/plugins/plugin-photo-swipe/src/client/helpers/index.ts @@ -0,0 +1 @@ +export * from './photo-swipe.js' diff --git a/plugins/plugin-photo-swipe/src/client/helpers/photo-swipe.ts b/plugins/plugin-photo-swipe/src/client/helpers/photo-swipe.ts new file mode 100644 index 0000000000..e8968e24b3 --- /dev/null +++ b/plugins/plugin-photo-swipe/src/client/helpers/photo-swipe.ts @@ -0,0 +1,26 @@ +import type { PhotoSwipeOptions as OriginalPhotoSwipeOptions } from 'photoswipe' +import type { App } from 'vue' +import { inject } from 'vue' + +export type PhotoSwipeOptions = Omit< + OriginalPhotoSwipeOptions, + // These are handled internally + 'dataSource' | 'index' +> + +declare const __VUEPRESS_DEV__: boolean + +let photoswipeOptions: PhotoSwipeOptions = {} + +const photoswipeSymbol = Symbol(__VUEPRESS_DEV__ ? 'photoswipe' : '') + +export const definePhotoSwipeConfig = (options: PhotoSwipeOptions): void => { + photoswipeOptions = options +} + +export const usePhotoSwipeOptions = (): PhotoSwipeOptions => + inject(photoswipeSymbol)! + +export const injectPhotoSwipeConfig = (app: App): void => { + app.provide(photoswipeSymbol, photoswipeOptions) +} diff --git a/plugins/plugin-photo-swipe/src/client/index.ts b/plugins/plugin-photo-swipe/src/client/index.ts new file mode 100644 index 0000000000..b2f2024fd1 --- /dev/null +++ b/plugins/plugin-photo-swipe/src/client/index.ts @@ -0,0 +1,3 @@ +export * from './helpers/index.js' +export * from './composables/index.js' +export * from './utils/index.js' diff --git a/plugins/plugin-photo-swipe/src/client/styles/photo-swipe.css b/plugins/plugin-photo-swipe/src/client/styles/photo-swipe.css new file mode 100644 index 0000000000..113d8ac55d --- /dev/null +++ b/plugins/plugin-photo-swipe/src/client/styles/photo-swipe.css @@ -0,0 +1,38 @@ +.photo-swipe-loading { + position: absolute; + inset: 0; + + display: flex; + align-items: center; + justify-content: center; +} + +.photo-swipe-bullets-indicator { + position: absolute; + bottom: 30px; + left: 50%; + + display: flex; + flex-direction: row; + align-items: center; + + transform: translate(-50%, 0); +} + +.photo-swipe-bullet { + width: 12px; + height: 6px; + margin: 0 5px; + border-radius: 3px; + + background: var(--photo-swipe-bullet); + + transition: + width 0.3s, + color 0.3s; +} + +.photo-swipe-bullet.active { + width: 30px; + background: var(--photo-swipe-bullet-active); +} diff --git a/plugins/plugin-photo-swipe/src/client/styles/vars.css b/plugins/plugin-photo-swipe/src/client/styles/vars.css new file mode 100644 index 0000000000..b721a131d3 --- /dev/null +++ b/plugins/plugin-photo-swipe/src/client/styles/vars.css @@ -0,0 +1,4 @@ +:root { + --photo-swipe-bullet: #fff; + --photo-swipe-bullet-active: #3eaf7c; +} diff --git a/plugins/plugin-photo-swipe/src/client/utils/createPhotoSwipe.ts b/plugins/plugin-photo-swipe/src/client/utils/createPhotoSwipe.ts new file mode 100644 index 0000000000..44c244e3ba --- /dev/null +++ b/plugins/plugin-photo-swipe/src/client/utils/createPhotoSwipe.ts @@ -0,0 +1,66 @@ +import { useEventListener } from '@vueuse/core' +import type PhotoSwipe from 'photoswipe' +import type { SlideData } from 'photoswipe' +import type { PhotoSwipeOptions } from '../helpers/index.js' +import { LOADING_ICON } from './icon.js' +import { getImageUrlInfo } from './images.js' +import { initPhotoSwipe } from './initPhotoSwipe.js' + +export interface PhotoSwipeState { + open: (index: number) => void + close: () => void + destroy: () => void +} + +export const createPhotoSwipe = ( + images: string[], + photoSwipeOptions: PhotoSwipeOptions, + scrollToClose = true, +): Promise => + import(/* webpackChunkName: "photo-swipe" */ 'photoswipe').then( + ({ default: PhotoSwipe }) => { + let currentPhotoSwipe: PhotoSwipe | null = null + + const dataSource = images.map((image) => ({ + html: LOADING_ICON, + msrc: image, + })) + + images.forEach((image, index) => { + getImageUrlInfo(image).then((data) => { + dataSource.splice(index, 1, data) + currentPhotoSwipe?.refreshSlideContent(index) + }) + }) + + const destroy = useEventListener('wheel', () => { + currentPhotoSwipe?.close() + }) + + return { + open: (index: number): void => { + currentPhotoSwipe?.close() + + currentPhotoSwipe = new PhotoSwipe({ + preloaderDelay: 0, + showHideAnimationType: 'zoom', + ...photoSwipeOptions, + dataSource, + index, + ...(scrollToClose + ? { closeOnVerticalDrag: true, wheelToZoom: false } + : {}), + }) + + initPhotoSwipe(currentPhotoSwipe) + + currentPhotoSwipe.addFilter('placeholderSrc', () => images[index]) + currentPhotoSwipe.init() + }, + close: (): void => { + currentPhotoSwipe?.close() + }, + destroy, + } + }, + ) diff --git a/plugins/plugin-photo-swipe/src/client/utils/icon.ts b/plugins/plugin-photo-swipe/src/client/utils/icon.ts new file mode 100644 index 0000000000..47faeb8fab --- /dev/null +++ b/plugins/plugin-photo-swipe/src/client/utils/icon.ts @@ -0,0 +1,2 @@ +export const LOADING_ICON = + '
' diff --git a/plugins/plugin-photo-swipe/src/client/utils/images.ts b/plugins/plugin-photo-swipe/src/client/utils/images.ts new file mode 100644 index 0000000000..88d089fbe7 --- /dev/null +++ b/plugins/plugin-photo-swipe/src/client/utils/images.ts @@ -0,0 +1,40 @@ +import { isString } from '@vuepress/helper/client' +import type { SlideData } from 'photoswipe' + +export const getImages = (selector: string | string[]): HTMLImageElement[] => + isString(selector) + ? Array.from(document.querySelectorAll(selector)) + : selector + .map((item) => + Array.from(document.querySelectorAll(item)), + ) + .flat() + +export const getImageElementInfo = ( + image: HTMLImageElement, +): Promise => + new Promise((resolve, reject) => { + if (image.complete) { + resolve({ + type: 'image', + element: image, + src: image.src, + width: image.naturalWidth, + height: image.naturalHeight, + alt: image.alt, + msrc: image.src, + }) + } else { + image.onload = (): void => resolve(getImageElementInfo(image)) + image.onerror = (err): void => reject(err) + } + }) + +export const getImageUrlInfo = (image: string): Promise => + new Promise((resolve, reject) => { + const el = new Image() + + el.src = image + el.onload = (): void => resolve(getImageElementInfo(el)) + el.onerror = (err): void => reject(err) + }) diff --git a/plugins/plugin-photo-swipe/src/client/utils/index.ts b/plugins/plugin-photo-swipe/src/client/utils/index.ts new file mode 100644 index 0000000000..c5d849d4d1 --- /dev/null +++ b/plugins/plugin-photo-swipe/src/client/utils/index.ts @@ -0,0 +1,3 @@ +export * from './createPhotoSwipe.js' +export * from './images.js' +export * from './usePhotoSwipe.js' diff --git a/plugins/plugin-photo-swipe/src/client/utils/initPhotoSwipe.ts b/plugins/plugin-photo-swipe/src/client/utils/initPhotoSwipe.ts new file mode 100644 index 0000000000..dcd3e335b2 --- /dev/null +++ b/plugins/plugin-photo-swipe/src/client/utils/initPhotoSwipe.ts @@ -0,0 +1,77 @@ +import { useFullscreen } from '@vueuse/core' +import type PhotoSwipe from 'photoswipe' + +export const initPhotoSwipe = (photoSwipe: PhotoSwipe): void => { + const { isSupported, toggle } = useFullscreen() + + photoSwipe.on('uiRegister', () => { + if (isSupported.value) + // add fullscreen button + photoSwipe.ui!.registerElement({ + name: 'fullscreen', + order: 7, + isButton: true, + + html: '', + + onClick: () => { + toggle() + }, + }) + + // add download button + photoSwipe.ui!.registerElement({ + name: 'download', + order: 8, + isButton: true, + tagName: 'a', + + // SVG with outline + html: { + isCustomSVG: true, + inner: + '', + outlineID: 'pswp__icn-download', + }, + + onInit: (el, photoSwipe) => { + el.setAttribute('download', '') + el.setAttribute('target', '_blank') + el.setAttribute('rel', 'noopener') + + photoSwipe.on('change', () => { + el.setAttribute('href', photoSwipe.currSlide!.data.src!) + }) + }, + }) + + // add bullets indicator + photoSwipe.ui!.registerElement({ + name: 'bulletsIndicator', + className: 'photo-swipe-bullets-indicator', + appendTo: 'wrapper', + onInit: (el, photoSwipe) => { + const bullets: HTMLElement[] = [] + let prevIndex = -1 + + for (let i = 0; i < photoSwipe.getNumItems(); i++) { + const bullet = document.createElement('div') + + bullet.className = 'photo-swipe-bullet' + bullet.onclick = (event: Event): void => { + photoSwipe.goTo(bullets.indexOf(event.target as HTMLElement)) + } + bullets.push(bullet) + el.appendChild(bullet) + } + + photoSwipe.on('change', () => { + if (prevIndex >= 0) bullets[prevIndex].classList.remove('active') + + bullets[photoSwipe.currIndex].classList.add('active') + prevIndex = photoSwipe.currIndex + }) + }, + }) + }) +} diff --git a/plugins/plugin-photo-swipe/src/client/utils/usePhotoSwipe.ts b/plugins/plugin-photo-swipe/src/client/utils/usePhotoSwipe.ts new file mode 100644 index 0000000000..dff5478dde --- /dev/null +++ b/plugins/plugin-photo-swipe/src/client/utils/usePhotoSwipe.ts @@ -0,0 +1,71 @@ +import { useEventListener } from '@vueuse/core' +import type PhotoSwipe from 'photoswipe' +import type { SlideData } from 'photoswipe' +import type { PhotoSwipeOptions } from '../helpers/index.js' +import { LOADING_ICON } from './icon.js' +import { getImageElementInfo } from './images.js' +import { initPhotoSwipe } from './initPhotoSwipe.js' + +export const registerPhotoSwipe = ( + images: HTMLImageElement[], + photoSwipeOptions: PhotoSwipeOptions, + scrollToClose = true, +): Promise<() => void> => + import(/* webpackChunkName: "photo-swipe" */ 'photoswipe').then( + ({ default: PhotoSwipe }) => { + let currentPhotoSwipe: PhotoSwipe | null = null + + const dataSource = images.map((image) => ({ + html: LOADING_ICON, + element: image, + msrc: image.src, + })) + + images.forEach((image, index) => { + const handler = (): void => { + currentPhotoSwipe?.destroy() + currentPhotoSwipe = new PhotoSwipe({ + preloaderDelay: 0, + showHideAnimationType: 'zoom', + ...photoSwipeOptions, + dataSource, + index, + ...(scrollToClose + ? { closeOnVerticalDrag: true, wheelToZoom: false } + : {}), + }) + + initPhotoSwipe(currentPhotoSwipe) + + currentPhotoSwipe.addFilter('thumbEl', () => image) + currentPhotoSwipe.addFilter('placeholderSrc', () => image.src) + currentPhotoSwipe.init() + } + + if (!image.getAttribute('photo-swipe')) { + image.style.cursor = 'zoom-in' + image.addEventListener('click', () => { + handler() + }) + image.addEventListener('keypress', ({ key }) => { + if (key === 'Enter') handler() + }) + // avoid registering multiple times + image.setAttribute('photo-swipe', '') + } + + getImageElementInfo(image).then((data) => { + dataSource.splice(index, 1, data) + currentPhotoSwipe?.refreshSlideContent(index) + }) + }) + + return scrollToClose + ? useEventListener('wheel', () => { + currentPhotoSwipe?.close() + }) + : (): void => { + // do nothing + } + }, + ) diff --git a/plugins/plugin-photo-swipe/src/node/index.ts b/plugins/plugin-photo-swipe/src/node/index.ts new file mode 100644 index 0000000000..0958bd3e5b --- /dev/null +++ b/plugins/plugin-photo-swipe/src/node/index.ts @@ -0,0 +1,5 @@ +export * from './locales.js' +export * from './photoSwipePlugin.js' + +export type * from './options.js' +export type * from '../shared/index.js' diff --git a/plugins/plugin-photo-swipe/src/node/locales.ts b/plugins/plugin-photo-swipe/src/node/locales.ts new file mode 100644 index 0000000000..7aa9fd1b61 --- /dev/null +++ b/plugins/plugin-photo-swipe/src/node/locales.ts @@ -0,0 +1,183 @@ +import type { PhotoSwipeLocaleConfig } from '../shared/index.js' + +export const photoSwipeLocales: PhotoSwipeLocaleConfig = { + '/en/': { + close: 'Close', + download: 'Download Image', + fullscreen: 'Switch to full screen', + zoom: 'Zoom in/out', + arrowPrev: 'Prev (Arrow Left)', + arrowNext: 'Next (Arrow Right)', + }, + + '/zh/': { + close: '关闭', + download: '下载图片', + fullscreen: '切换全屏', + zoom: '缩放', + arrowPrev: '上一个 (左箭头)', + arrowNext: '下一个 (右箭头)', + }, + + '/zh-tw/': { + close: '關閉', + download: '下載圖片', + fullscreen: '切換全屏', + zoom: '縮放', + arrowPrev: '上一個 (左箭頭)', + arrowNext: '下一個 (右箭頭)', + }, + + '/de/': { + close: 'Schließen', + download: 'Download', + fullscreen: 'Vollbild aktivieren', + zoom: 'Rein / rauszoomen', + arrowPrev: 'Zurück (Pfeil links)', + arrowNext: 'Weiter (Pfeil rechts)', + }, + + '/de-at/': { + close: 'Schließen', + download: 'Download', + fullscreen: 'Toggle fullscreen', + zoom: 'Rein / rauszoomen', + arrowPrev: 'Zurück (Pfeil links)', + arrowNext: 'Weiter (Pfeil rechts)', + }, + + '/vi/': { + close: 'Đóng', + download: 'download', + fullscreen: 'Bật chế độ toàn màn hình', + zoom: 'Phóng to / thu nhỏ', + arrowPrev: 'Trước (Mũi tên trái)', + arrowNext: 'Tiếp theo (Mũi tên Phải)', + }, + + '/uk/': { + close: 'Закрити', + download: 'Завантажити зображення', + fullscreen: 'Перейти на повний екран', + zoom: 'Збільшити/Зменшити', + arrowPrev: 'Попередня (Стрілка вліво)', + arrowNext: 'Далі (стрілка вправо)', + }, + + '/ru/': { + close: 'Закрыть', + download: 'Загрузить изображение', + fullscreen: 'Переключиться на полный экран', + zoom: 'Увеличить/Уменьшить', + arrowPrev: 'Предыдущая (Стрелка влево)', + arrowNext: 'Следующая (Стрелка вправо)', + }, + + '/br/': { + close: 'Fechar', + download: 'Baixar imagem', + fullscreen: 'Alternar para tela cheia', + zoom: 'Aproximar mais/menos', + arrowPrev: 'Anterior (Seta Esquerda)', + arrowNext: 'Próximo (Seta Direita)', + }, + + '/pl/': { + close: 'Zamknij', + download: 'Pobierz obraz', + fullscreen: 'Przełącz na pełny ekran', + zoom: 'Powiększ/pomniejsz', + arrowPrev: 'Poprzedni (strzałka w lewo)', + arrowNext: 'Następny (strzałka w prawo)', + }, + + '/sk/': { + close: 'Zatvor', + download: 'Stiahni obrázok', + fullscreen: 'Prepni na celú obrazovku', + zoom: 'Priblíž/Oddial', + arrowPrev: 'Predošlí (šípka doľava)', + arrowNext: 'Nasledujúci (šípka doprava)', + }, + + '/fr/': { + close: 'Fermer', + download: "Télécharger l'image", + fullscreen: 'Basculer en plein écran', + zoom: 'Zoom avant/arrière', + arrowPrev: 'Précédent (Flèche gauche)', + arrowNext: 'Suivant (Flèche droite)', + }, + + '/es/': { + close: 'Cerrar', + download: 'Descargar imagen', + fullscreen: 'Cambiar a pantalla completa', + zoom: 'Acercar/Alejar', + arrowPrev: 'Anterior (Flecha izquierda)', + arrowNext: 'Siguiente (Flecha derecha)', + }, + + '/ja/': { + close: '閉じる', + download: '画像ダウンロード', + fullscreen: '全画面表示への切り替え', + zoom: '拡大・縮小', + arrowPrev: '前へ(左矢印)', + arrowNext: '次へ(右矢印)', + }, + + '/tr/': { + close: 'Kapat', + download: 'Resmi indir', + fullscreen: 'Tam ekrana geç', + zoom: 'Yakınlaştır/Uzaklaştır', + arrowPrev: 'Önceki (Sol ok)', + arrowNext: 'Sonraki (Sağ ok)', + }, + + '/ko/': { + close: '닫기', + download: '이미지 다운로드', + fullscreen: '전체 화면 전환', + zoom: '확대/축소', + arrowPrev: '이전 (왼쪽 화살표)', + arrowNext: '다음 (오른쪽 화살표)', + }, + + '/fi/': { + close: 'Sulje', + download: 'Lataa kuva', + fullscreen: 'Vaihda kokoruututilaan', + zoom: 'Lähennä/Työnnä', + arrowPrev: 'Edellinen (Vasen nuoli)', + arrowNext: 'Seuraava (Oikea nuoli)', + }, + + '/hu/': { + close: 'Bezárás', + download: 'Kép letöltése', + fullscreen: 'Váltás teljes képernyőre', + zoom: 'Nagyítás/kicsinyítés', + arrowPrev: 'Előző (Balra nyíl)', + arrowNext: 'Következő (Jobbra nyíl)', + }, + + '/id/': { + close: 'Tutup', + download: 'Unduh gambar', + fullscreen: 'Beralih ke layar penuh', + zoom: 'Perbesar/Perkecil', + arrowPrev: 'Sebelumnya (Panah kiri)', + arrowNext: 'Selanjutnya (Panah kanan)', + }, + + '/nl/': { + close: 'Sluiten', + download: 'Download Image', + fullscreen: 'Verander naar fullscreen', + zoom: 'Zoom in/out', + arrowPrev: 'Vorige (Pijl Links)', + arrowNext: 'Volgende (Pijl Rechts)', + }, +} diff --git a/plugins/plugin-photo-swipe/src/node/logger.ts b/plugins/plugin-photo-swipe/src/node/logger.ts new file mode 100644 index 0000000000..5a4eafc9cb --- /dev/null +++ b/plugins/plugin-photo-swipe/src/node/logger.ts @@ -0,0 +1,5 @@ +import { Logger } from '@vuepress/helper' + +export const PLUGIN_NAME = '@vuepress/plugin-photo-swipe' + +export const logger = new Logger(PLUGIN_NAME) diff --git a/plugins/plugin-photo-swipe/src/node/options.ts b/plugins/plugin-photo-swipe/src/node/options.ts new file mode 100644 index 0000000000..1752b5cf7f --- /dev/null +++ b/plugins/plugin-photo-swipe/src/node/options.ts @@ -0,0 +1,42 @@ +import type { LocaleConfig } from 'vuepress/shared' +import type { PhotoSwipePluginLocaleData } from '../shared/index.js' + +export interface PhotoSwipePluginOptions { + /** + * Image selector + * + * 图片选择器 + * + * @default ".theme-default-content :not(a) > img:not([no-view])" + */ + selector?: string | string[] + + /** + * Whether close the current image when scrolling. + * + * 是否在滚动时关闭当前图片。 + * + * @default true + */ + scrollToClose?: boolean + + /** + * The delay of photo-swipe fetching page images, in ms + * + * If the theme you are using has a switching animation, it is recommended to configure this option to `Switch animation duration + 200` + * + * photo-swipe 抓取页面图片的延时,单位 ms + * + * 如果你使用的主题有切换动画,建议配置此选项为 `切换动画时长 + 200` + * + * @default 800 + */ + delay?: number + + /** + * Locale config + * + * 国际化配置 + */ + locales?: LocaleConfig +} diff --git a/plugins/plugin-photo-swipe/src/node/photoSwipePlugin.ts b/plugins/plugin-photo-swipe/src/node/photoSwipePlugin.ts new file mode 100644 index 0000000000..bf2b753def --- /dev/null +++ b/plugins/plugin-photo-swipe/src/node/photoSwipePlugin.ts @@ -0,0 +1,57 @@ +import { + addViteOptimizeDepsExclude, + addViteSsrNoExternal, + entries, + fromEntries, + getLocaleConfig, +} from '@vuepress/helper' +import type { PluginFunction } from 'vuepress/core' +import { getDirname, path } from 'vuepress/utils' +import { photoSwipeLocales } from './locales.js' +import { logger, PLUGIN_NAME } from './logger.js' +import type { PhotoSwipePluginOptions } from './options.js' + +const __dirname = getDirname(import.meta.url) + +export const photoSwipePlugin = + (options: PhotoSwipePluginOptions = {}): PluginFunction => + (app) => { + if (app.env.isDebug) logger.info('Options:', options) + + return { + name: PLUGIN_NAME, + + define: (app): Record => ({ + __PS_SELECTOR__: + options.selector || + '.theme-default-content :not(a) > img:not([no-view])', + __PS_DELAY__: options.delay || 800, + __PS_SCROLL_TO_CLOSE__: options.scrollToClose ?? true, + __PS_LOCALES__: fromEntries( + entries( + getLocaleConfig({ + app, + name: PLUGIN_NAME, + default: photoSwipeLocales, + config: options.locales, + }), + ).map(([localePath, localeOptions]) => [ + localePath, + fromEntries( + entries(localeOptions).map(([key, value]) => [ + `${key}Title`, + value, + ]), + ), + ]), + ), + }), + + extendsBundlerOptions: (bundlerOptions: unknown, app): void => { + addViteOptimizeDepsExclude(bundlerOptions, app, 'photoswipe') + addViteSsrNoExternal(bundlerOptions, app, '@vuepress/helper') + }, + + clientConfigFile: path.resolve(__dirname, '../client/config.js'), + } + } diff --git a/plugins/plugin-photo-swipe/src/shared/index.ts b/plugins/plugin-photo-swipe/src/shared/index.ts new file mode 100644 index 0000000000..3b157fa5a9 --- /dev/null +++ b/plugins/plugin-photo-swipe/src/shared/index.ts @@ -0,0 +1 @@ +export * from './locales.js' diff --git a/plugins/plugin-photo-swipe/src/shared/locales.ts b/plugins/plugin-photo-swipe/src/shared/locales.ts new file mode 100644 index 0000000000..522ed07d9e --- /dev/null +++ b/plugins/plugin-photo-swipe/src/shared/locales.ts @@ -0,0 +1,48 @@ +import type { ExactLocaleConfig } from '@vuepress/helper' + +export interface PhotoSwipePluginLocaleData { + /** + * Close button label text + * + * 关闭按钮标签文字 + */ + close: string + + /** + * Download button label text + * + * 下载按钮标签文字 + */ + download: string + + /** + * Full screen button label text + * + * 全屏按钮标签文字 + */ + fullscreen: string + + /** + * Zoom button label text + * + * 缩放按钮标签文字 + */ + zoom: string + + /** + * Previous image button label text + * + * 上一张图片按钮标签文字 + */ + arrowPrev: string + + /** + * Next image button label text + * + * 下一张图片按钮标签文字 + */ + arrowNext: string +} + +export type PhotoSwipeLocaleConfig = + ExactLocaleConfig diff --git a/plugins/plugin-photo-swipe/tsconfig.build.json b/plugins/plugin-photo-swipe/tsconfig.build.json new file mode 100644 index 0000000000..e0a82d8177 --- /dev/null +++ b/plugins/plugin-photo-swipe/tsconfig.build.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.build.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "types": ["vuepress/client-types"] + }, + "include": ["./src"], + "references": [{ "path": "../../tools/helper/tsconfig.build.json" }] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 779e901b68..b5a12982e5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -116,6 +116,9 @@ importers: '@vuepress/plugin-nprogress': specifier: workspace:* version: link:../plugins/plugin-nprogress + '@vuepress/plugin-photo-swipe': + specifier: workspace:* + version: link:../plugins/plugin-photo-swipe '@vuepress/plugin-pwa-popup': specifier: workspace:* version: link:../plugins/plugin-pwa-popup @@ -410,6 +413,24 @@ importers: specifier: 2.0.0-rc.6 version: 2.0.0-rc.6(@vuepress/bundler-vite@2.0.0-rc.6)(@vuepress/bundler-webpack@2.0.0-rc.6)(typescript@5.3.3)(vue@3.4.15) + plugins/plugin-photo-swipe: + dependencies: + '@vuepress/helper': + specifier: workspace:~2.0.0-rc.11 + version: link:../../tools/helper + '@vueuse/core': + specifier: ^10.7.2 + version: 10.7.2(vue@3.4.15) + photoswipe: + specifier: ^5.4.3 + version: 5.4.3 + vue: + specifier: ^3.4.15 + version: 3.4.15(typescript@5.3.3) + vuepress: + specifier: 2.0.0-rc.6 + version: 2.0.0-rc.6(@vuepress/bundler-vite@2.0.0-rc.6)(@vuepress/bundler-webpack@2.0.0-rc.6)(typescript@5.3.3)(vue@3.4.15) + plugins/plugin-prismjs: dependencies: prismjs: @@ -9501,6 +9522,11 @@ packages: resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} dev: true + /photoswipe@5.4.3: + resolution: {integrity: sha512-9UC6oJBK4oXFZ5HcdlcvGkfEHsVrmE4csUdCQhEjHYb3PvPLO3PG7UhnPuOgjxwmhq5s17Un5NUdum01LgBDng==} + engines: {node: '>= 0.12.0'} + dev: false + /picocolors@1.0.0: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}