feat(plugin-md-power): add lazy animation mode for mark highlights (#718)
This commit is contained in:
parent
aa9c64f00f
commit
5c0d211d82
@ -119,6 +119,12 @@ export default defineUserConfig({
|
||||
- **默认值**: `false`
|
||||
- **详情**: 是否启用缩写语法
|
||||
|
||||
### mark
|
||||
|
||||
- **类型**: `MarkOptions`
|
||||
- **默认值**: `'eager'`
|
||||
- **详情**: 设置 `==马克笔==` 的动画播放模式
|
||||
|
||||
### codeTabs
|
||||
|
||||
- **类型**: `boolean | CodeTabsOptions`
|
||||
|
||||
@ -129,6 +129,12 @@ The `include` configuration is implemented by the
|
||||
- **Default:** `false`
|
||||
- **Details:** Whether to enable abbreviation syntax.
|
||||
|
||||
### mark
|
||||
|
||||
- **Type:** `MarkOptions`
|
||||
- **Default:** `'eager'`
|
||||
- **Details:** Sets when the `==mark==` animation runs.
|
||||
|
||||
### codeTabs
|
||||
|
||||
- **Type:** `boolean | CodeTabsOptions`
|
||||
|
||||
@ -81,6 +81,15 @@ mark {
|
||||
--vp-mark-bg-shift: 0.55lh;
|
||||
--vp-mark-linear-color: var(--vp-c-brand-3);
|
||||
--vp-mark-bg-image: linear-gradient(to right, var(--vp-mark-linear-color) 50%, transparent 50%);
|
||||
animation: var(--vp-mark-animation, mark-highlight 1.5s 0.5s forwards);
|
||||
}
|
||||
|
||||
[data-mark-mode="lazy"] mark {
|
||||
--vp-mark-animation: none;
|
||||
}
|
||||
|
||||
[data-mark-mode="lazy"] mark.vp-mark-visible {
|
||||
animation: mark-highlight 1.5s 0.2s forwards;
|
||||
}
|
||||
|
||||
mark.note {
|
||||
@ -145,3 +154,19 @@ mark.classname {
|
||||
Use `==Mark=={.classname}` in Markdown.
|
||||
|
||||
You can name `classname` freely and add other CSS properties besides modifying CSS variables.
|
||||
|
||||
## Animation Modes
|
||||
|
||||
By default, the highlight animation plays as soon as the page renders.
|
||||
|
||||
If you prefer to animate only when the marker enters the viewport, set `markdown.mark` to `'lazy'` in your theme config:
|
||||
|
||||
```ts title=".vuepress/config.ts" {5}
|
||||
export default defineUserConfig({
|
||||
theme: plumeTheme({
|
||||
markdown: {
|
||||
mark: 'lazy',
|
||||
},
|
||||
}),
|
||||
})
|
||||
```
|
||||
|
||||
@ -82,6 +82,15 @@ mark {
|
||||
--vp-mark-bg-shift: 0.55lh;
|
||||
--vp-mark-linear-color: var(--vp-c-brand-3);
|
||||
--vp-mark-bg-image: linear-gradient(to right, var(--vp-mark-linear-color) 50%, transparent 50%);
|
||||
animation: var(--vp-mark-animation, mark-highlight 1.5s 0.5s forwards);
|
||||
}
|
||||
|
||||
[data-mark-mode="lazy"] mark {
|
||||
--vp-mark-animation: none;
|
||||
}
|
||||
|
||||
[data-mark-mode="lazy"] mark.vp-mark-visible {
|
||||
animation: mark-highlight 1.5s 0.2s forwards;
|
||||
}
|
||||
|
||||
mark.note {
|
||||
@ -146,3 +155,19 @@ mark.classname {
|
||||
然后在 Markdown 中使用 `==Mark=={.classname}` 进行标记。
|
||||
|
||||
你可以随意命名 `classname`,除了修改 CSS 变量,也可以添加其他的 CSS 样式属性。
|
||||
|
||||
## 动画模式
|
||||
|
||||
默认情况下,马克笔会在页面渲染时立即播放描线动画。
|
||||
|
||||
如果希望在滚动到可视区域后再播放动画,可以在主题配置中将 `markdown.mark` 设置为 `'lazy'`:
|
||||
|
||||
```ts title=".vuepress/config.ts" {5}
|
||||
export default defineUserConfig({
|
||||
theme: plumeTheme({
|
||||
markdown: {
|
||||
mark: 'lazy',
|
||||
},
|
||||
}),
|
||||
})
|
||||
```
|
||||
|
||||
140
plugins/plugin-md-power/src/client/composables/mark.ts
Normal file
140
plugins/plugin-md-power/src/client/composables/mark.ts
Normal file
@ -0,0 +1,140 @@
|
||||
import { onBeforeUnmount, onMounted } from 'vue'
|
||||
import { onContentUpdated, useRouter } from 'vuepress/client'
|
||||
|
||||
const MARK_MODE_ATTR = 'data-mark-mode'
|
||||
const MARK_MODE_LAZY = 'lazy'
|
||||
const MARK_VISIBLE_CLASS = 'vp-mark-visible'
|
||||
const MARK_BOUND_ATTR = 'data-vp-mark-bound'
|
||||
const MARK_SELECTOR = '.vp-doc mark'
|
||||
const DOC_SELECTOR = '.vp-doc'
|
||||
const BOUND_SELECTOR = `${MARK_SELECTOR}[${MARK_BOUND_ATTR}="1"]`
|
||||
|
||||
export function setupMarkHighlight(mode: 'lazy' | 'eager'): void {
|
||||
if (typeof window === 'undefined' || __VUEPRESS_SSR__)
|
||||
return
|
||||
|
||||
const root = document.documentElement
|
||||
|
||||
if (mode !== MARK_MODE_LAZY) {
|
||||
root.removeAttribute(MARK_MODE_ATTR)
|
||||
return
|
||||
}
|
||||
|
||||
root.setAttribute(MARK_MODE_ATTR, MARK_MODE_LAZY)
|
||||
|
||||
let intersectionObserver: IntersectionObserver | null = null
|
||||
let mutationObserver: MutationObserver | null = null
|
||||
let rafId: number | null = null
|
||||
let removeAfterEach: (() => void) | null = null
|
||||
|
||||
const ensureObserver = () => {
|
||||
if (!intersectionObserver) {
|
||||
intersectionObserver = new IntersectionObserver((entries, obs) => {
|
||||
for (const entry of entries) {
|
||||
if (!entry.isIntersecting && entry.intersectionRatio <= 0)
|
||||
continue
|
||||
|
||||
const target = entry.target as HTMLElement
|
||||
target.classList.add(MARK_VISIBLE_CLASS)
|
||||
target.removeAttribute(MARK_BOUND_ATTR)
|
||||
obs.unobserve(target)
|
||||
}
|
||||
}, {
|
||||
threshold: [0, 0.1, 0.25, 0.5],
|
||||
rootMargin: '8% 0px -8% 0px',
|
||||
})
|
||||
}
|
||||
return intersectionObserver
|
||||
}
|
||||
|
||||
const bindMarks = () => {
|
||||
const marks = Array.from(document.querySelectorAll<HTMLElement>(MARK_SELECTOR))
|
||||
.filter(mark =>
|
||||
mark instanceof HTMLElement
|
||||
&& !mark.classList.contains(MARK_VISIBLE_CLASS)
|
||||
&& mark.getAttribute(MARK_BOUND_ATTR) !== '1',
|
||||
)
|
||||
|
||||
if (marks.length === 0)
|
||||
return
|
||||
|
||||
const observer = ensureObserver()
|
||||
for (const mark of marks) {
|
||||
mark.setAttribute(MARK_BOUND_ATTR, '1')
|
||||
observer.observe(mark)
|
||||
}
|
||||
}
|
||||
|
||||
const scheduleBind = () => {
|
||||
if (rafId !== null)
|
||||
cancelAnimationFrame(rafId)
|
||||
|
||||
rafId = requestAnimationFrame(() => {
|
||||
rafId = null
|
||||
bindMarks()
|
||||
})
|
||||
}
|
||||
|
||||
const observeDocMutations = () => {
|
||||
const doc = document.querySelector(DOC_SELECTOR)
|
||||
if (!doc)
|
||||
return
|
||||
|
||||
if (mutationObserver)
|
||||
mutationObserver.disconnect()
|
||||
|
||||
mutationObserver = new MutationObserver((mutations) => {
|
||||
if (mutations.some(mutation => mutation.addedNodes.length > 0))
|
||||
scheduleBind()
|
||||
})
|
||||
mutationObserver.observe(doc, { childList: true, subtree: true })
|
||||
}
|
||||
|
||||
const resetObserver = () => {
|
||||
document.querySelectorAll<HTMLElement>(BOUND_SELECTOR).forEach((mark) => {
|
||||
if (!mark.classList.contains(MARK_VISIBLE_CLASS))
|
||||
mark.removeAttribute(MARK_BOUND_ATTR)
|
||||
})
|
||||
|
||||
if (intersectionObserver) {
|
||||
intersectionObserver.disconnect()
|
||||
intersectionObserver = null
|
||||
}
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
onMounted(() => {
|
||||
observeDocMutations()
|
||||
scheduleBind()
|
||||
})
|
||||
|
||||
onContentUpdated(() => {
|
||||
resetObserver()
|
||||
observeDocMutations()
|
||||
scheduleBind()
|
||||
})
|
||||
|
||||
if (router?.afterEach) {
|
||||
removeAfterEach = router.afterEach(() => {
|
||||
resetObserver()
|
||||
observeDocMutations()
|
||||
scheduleBind()
|
||||
})
|
||||
}
|
||||
|
||||
if (router?.isReady) {
|
||||
router.isReady().then(() => scheduleBind()).catch(() => {})
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (rafId !== null)
|
||||
cancelAnimationFrame(rafId)
|
||||
|
||||
resetObserver()
|
||||
mutationObserver?.disconnect()
|
||||
mutationObserver = null
|
||||
removeAfterEach?.()
|
||||
removeAfterEach = null
|
||||
})
|
||||
}
|
||||
@ -132,6 +132,18 @@ export async function prepareConfigFile(app: App, options: MarkdownPowerPluginOp
|
||||
}
|
||||
|
||||
const setupIcon = prepareIcon(imports, options.icon)
|
||||
const setupStmts: string[] = []
|
||||
const iconSetup = setupIcon.trim()
|
||||
if (iconSetup)
|
||||
setupStmts.push(iconSetup)
|
||||
|
||||
const markMode = options.mark === 'lazy' ? 'lazy' : 'eager'
|
||||
imports.add(`import { setupMarkHighlight } from '${CLIENT_FOLDER}composables/mark.js'`)
|
||||
setupStmts.push(`setupMarkHighlight(${JSON.stringify(markMode)})`)
|
||||
|
||||
const setupContent = setupStmts.length
|
||||
? ` ${setupStmts.join('\n ')}\n`
|
||||
: ''
|
||||
|
||||
return app.writeTemp(
|
||||
'md-power/config.js',
|
||||
@ -148,7 +160,7 @@ ${Array.from(enhances.values())
|
||||
.join('\n')}
|
||||
},
|
||||
setup() {
|
||||
${setupIcon}
|
||||
${setupContent}
|
||||
}
|
||||
})
|
||||
`,
|
||||
|
||||
1
plugins/plugin-md-power/src/shared/mark.ts
Normal file
1
plugins/plugin-md-power/src/shared/mark.ts
Normal file
@ -0,0 +1 @@
|
||||
export type MarkOptions = 'lazy' | 'eager'
|
||||
@ -3,6 +3,7 @@ import type { CodeTabsOptions } from './codeTabs.js'
|
||||
import type { CodeTreeOptions } from './codeTree.js'
|
||||
import type { FileTreeOptions } from './fileTree.js'
|
||||
import type { IconOptions } from './icon.js'
|
||||
import type { MarkOptions } from './mark.js'
|
||||
import type { NpmToOptions } from './npmTo.js'
|
||||
import type { PDFOptions } from './pdf.js'
|
||||
import type { PlotOptions } from './plot.js'
|
||||
@ -21,6 +22,11 @@ export interface MarkdownPowerPluginOptions {
|
||||
* @default false
|
||||
*/
|
||||
abbr?: boolean
|
||||
/**
|
||||
* 马克笔动画模式
|
||||
* @default 'eager'
|
||||
*/
|
||||
mark?: MarkOptions
|
||||
/**
|
||||
* 配置代码块分组
|
||||
*/
|
||||
|
||||
@ -4,7 +4,7 @@ import { argv } from '../../scripts/tsdown-args.mjs'
|
||||
/** @import {Options} from 'tsdown' */
|
||||
|
||||
const config = [
|
||||
{ dir: 'composables', files: ['codeRepl.ts', 'pdf.ts', 'rustRepl.ts', 'size.ts', 'audio.ts', 'demo.ts'] },
|
||||
{ dir: 'composables', files: ['codeRepl.ts', 'pdf.ts', 'rustRepl.ts', 'size.ts', 'audio.ts', 'demo.ts', 'mark.ts'] },
|
||||
{ dir: 'utils', files: ['http.ts', 'link.ts', 'sleep.ts'] },
|
||||
{ dir: '', files: ['index.ts', 'options.ts'] },
|
||||
]
|
||||
|
||||
@ -15,7 +15,7 @@ mark {
|
||||
background-repeat: no-repeat;
|
||||
background-position: 100% var(--vp-mark-bg-shift);
|
||||
background-size: 200%;
|
||||
animation: mark-highlight 1.5s 0.5s forwards;
|
||||
animation: var(--vp-mark-animation, mark-highlight 1.5s 0.5s forwards);
|
||||
}
|
||||
|
||||
mark:where(.note) {
|
||||
@ -66,6 +66,14 @@ mark:where(.important) {
|
||||
--vp-mark-linear-color: #66c;
|
||||
}
|
||||
|
||||
[data-mark-mode="lazy"] mark {
|
||||
--vp-mark-animation: none;
|
||||
}
|
||||
|
||||
[data-mark-mode="lazy"] mark.vp-mark-visible {
|
||||
animation: mark-highlight 1.5s 0.2s forwards;
|
||||
}
|
||||
|
||||
@keyframes mark-highlight {
|
||||
0% {
|
||||
color: inherit;
|
||||
|
||||
@ -52,6 +52,7 @@ export const MARKDOWN_POWER_FIELDS: (keyof MarkdownPowerPluginOptions)[] = [
|
||||
'icon',
|
||||
'imageSize',
|
||||
'jsfiddle',
|
||||
'mark',
|
||||
'npmTo',
|
||||
'pdf',
|
||||
'plot',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user