import DefaultTheme from 'vitepress/theme'
import mediumZoom from 'medium-zoom'
import { onContentUpdated } from 'vitepress'
import { onMounted } from 'vue'
import type { Theme } from 'vitepress'
import './custom.css'
/**
* 参与「点击放大」的图片范围。
*
* `.vp-doc` 是 VitePress 默认主题给正文容器加的类名,使用手册 9 页与接口文档
* 全部页面共用这一套布局,所以在这里定义一次即对全站生效。
*
* 排除项:
* a img 图片本身是链接(如带外链的截图),点击应跳转而不是放大
* .no-zoom 需要单独关闭放大的图片,示例:
* `
`
*/
const ZOOM_SELECTOR = '.vp-doc img:not(a img):not(.no-zoom)'
/**
* 全站只保留一个 medium-zoom 实例,但【必须惰性创建】。
*
* 两个坑叠在一起,才得到这个写法:
*
* 1. 不能在模块顶层创建实例。medium-zoom 1.1.0 的工厂函数第一件事就是读
* `window.Promise`(dist/medium-zoom.min.js 里即 `var Promise = window.Promise || ...`),
* 而主题模块在 `vitepress build` 时会被 Node 端 SSR 打包并执行 —— 没有 window,
* 直接 `ReferenceError: window is not defined` 构建失败。
* 所以实例只能在浏览器生命周期回调里创建。
*
* 2. 也不能每次路由切换都 `mediumZoom(...)` 新建。每个实例都会在 document 上注册
* click / keyup / scroll、在 window 上注册 resize 监听器,且没有卸载入口
* (见 dist/medium-zoom.esm.js:576-581)。翻 N 个页面就会留下 N 组永不释放的
* 全局监听器。正确做法是复用实例,路由切换时只做 attach —— attach 内部会跳过
* 已挂载的图片(dist/medium-zoom.esm.js:199-204),因此重复调用是幂等的。
*/
let zoom: ReturnType | null = null
const attachZoom = () => {
if (typeof window === 'undefined') return
if (!zoom) {
zoom = mediumZoom({
background: 'var(--vp-c-bg)',
margin: 24,
})
}
zoom.attach(ZOOM_SELECTOR)
}
const closeZoomOnClick = (event: MouseEvent) => {
const target = event.target as Element | null
if (!zoom || !target) return
if (target.closest('.medium-zoom-image--opened, .medium-zoom-overlay')) {
void zoom.close()
}
}
const theme: Theme = {
extends: DefaultTheme,
setup() {
// onContentUpdated 由 VitePress 在「新页面内容挂载完成后」触发,覆盖首次进入
// 与 SPA 客户端路由切换。它挂在路由组件的 onVnodeMounted 上
// (见 vitepress/dist/client/app/components/Content.js:13-20),
// 比 watch(route.path) + nextTick 更可靠 —— 后者可能在异步页面组件挂载前就执行,
// 导致某些页面图片没挂上监听、点击放大静默失效。
onContentUpdated(attachZoom)
// 兜底:主题 setup 时内容已就绪(不经过 onVnodeMounted 的极端情况)也能挂上
onMounted(attachZoom)
if (typeof document !== 'undefined') {
document.addEventListener('click', closeZoomOnClick, true)
}
},
}
export default theme