返回 slidev
v-click.ts
根目录 / packages / client / modules / v-click.ts
1 import type { ClicksElement, RawAtValue } from '@slidev/types'
2 import type { App, DirectiveBinding } from 'vue'
3 import { computed, watchEffect } from 'vue'
4 import {
5 CLASS_VCLICK_ANIMATION_PREFIX,
6 CLASS_VCLICK_CURRENT,
7 CLASS_VCLICK_HIDDEN,
8 CLASS_VCLICK_HIDDEN_EXP,
9 CLASS_VCLICK_PRIOR,
10 CLASS_VCLICK_TARGET,
11 injectionClicksContext,
12 injectionFrontmatter,
13 } from '../constants'
14 import { configs } from '../env'
15 import { directiveInject } from '../utils'
16
17 const RE_WHITESPACE_OR_COMMA = /[\s,]+/
18
19 function syncAnimationClasses(el: HTMLElement, animations: string[]) {
20 const targetClasses = animations.map(a => `${CLASS_VCLICK_ANIMATION_PREFIX}${a}`)
21 el.classList.forEach((c) => {
22 if (c.startsWith(CLASS_VCLICK_ANIMATION_PREFIX) && !targetClasses.includes(c))
23 el.classList.remove(c)
24 })
25 targetClasses.forEach(c => el.classList.add(c))
26 }
27
28 export function createVClickDirectives() {
29 return {
30 install(app: App) {
31 app.directive<HTMLElement, RawAtValue>('click', {
32 // @ts-expect-error extra prop
33 name: 'v-click',
34
35 mounted(el, dir) {
36 const resolved = resolveClick(el, dir, dir.value)
37 if (resolved == null)
38 return
39
40 el.classList.toggle(CLASS_VCLICK_TARGET, true)
41
42 // Expose the resolved clicks info to the element to make it easier to understand and debug
43 el.dataset.slidevClicksStart = String(resolved.start)
44 if (Number.isFinite(resolved.end))
45 el.dataset.slidevClicksEnd = String(resolved.end)
46
47 // @ts-expect-error extra prop
48 el.watchStopHandle = watchEffect(() => {
49 const active = resolved.isActive.value
50 const current = resolved.isCurrent.value
51 const prior = active && !current
52
53 if (resolved.flagHide) {
54 el.classList.toggle(CLASS_VCLICK_HIDDEN, active)
55 el.classList.toggle(CLASS_VCLICK_HIDDEN_EXP, active)
56 }
57 else {
58 el.classList.toggle(CLASS_VCLICK_HIDDEN, !active)
59 }
60
61 syncAnimationClasses(el, resolved.flagAnimations.value)
62
63 el.classList.toggle(CLASS_VCLICK_CURRENT, current)
64 el.classList.toggle(CLASS_VCLICK_PRIOR, prior)
65 })
66 },
67 unmounted,
68 })
69
70 app.directive<HTMLElement, RawAtValue>('after', {
71 // @ts-expect-error extra prop
72 name: 'v-after',
73
74 mounted(el, dir) {
75 const resolved = resolveClick(el, dir, '+0')
76 if (resolved == null)
77 return
78
79 el.classList.toggle(CLASS_VCLICK_TARGET, true)
80
81 // @ts-expect-error extra prop
82 el.watchStopHandle = watchEffect(() => {
83 const active = resolved.isActive.value
84 const current = resolved.isCurrent.value
85 const prior = active && !current
86
87 if (resolved.flagHide) {
88 el.classList.toggle(CLASS_VCLICK_HIDDEN, active)
89 el.classList.toggle(CLASS_VCLICK_HIDDEN_EXP, active)
90 }
91 else {
92 el.classList.toggle(CLASS_VCLICK_HIDDEN, !active)
93 }
94
95 syncAnimationClasses(el, resolved.flagAnimations.value)
96
97 el.classList.toggle(CLASS_VCLICK_CURRENT, current)
98 el.classList.toggle(CLASS_VCLICK_PRIOR, prior)
99 })
100 },
101 unmounted,
102 })
103
104 app.directive<HTMLElement, RawAtValue>('click-hide', {
105 // @ts-expect-error extra prop
106 name: 'v-click-hide',
107
108 mounted(el, dir) {
109 const resolved = resolveClick(el, dir, dir.value, true)
110 if (resolved == null)
111 return
112
113 el.classList.toggle(CLASS_VCLICK_TARGET, true)
114
115 // @ts-expect-error extra prop
116 el.watchStopHandle = watchEffect(() => {
117 const active = resolved.isActive.value
118 const current = resolved.isCurrent.value
119 const prior = active && !current
120
121 el.classList.toggle(CLASS_VCLICK_HIDDEN, active)
122 el.classList.toggle(CLASS_VCLICK_HIDDEN_EXP, active)
123
124 syncAnimationClasses(el, resolved.flagAnimations.value)
125
126 el.classList.toggle(CLASS_VCLICK_CURRENT, current)
127 el.classList.toggle(CLASS_VCLICK_PRIOR, prior)
128 })
129 },
130 unmounted,
131 })
132 },
133 }
134 }
135
136 export const resolvedClickMap = new Map<ClicksElement, ReturnType<typeof resolveClick>>()
137
138 export function resolveClick(el: Element | string, dir: DirectiveBinding<any>, value: RawAtValue, explicitHide = false) {
139 const ctx = directiveInject(dir, injectionClicksContext)?.value
140 const frontmatter = directiveInject(dir, injectionFrontmatter)
141
142 if (!el || !ctx)
143 return null
144
145 const flagHide = explicitHide || (dir.modifiers.hide !== false && dir.modifiers.hide != null)
146
147 /**
148 * Resolves the animation presets for this element.
149 * Priority: directive modifiers (stacked) > slide frontmatter > global config.
150 * Modifiers allow composition, e.g., v-click.fade.up.scale.
151 */
152 const elModifiers = Object.keys({ ...dir.modifiers }).filter(m => m !== 'hide')
153 const flagAnimations = computed(() => {
154 if (elModifiers.length > 0)
155 return elModifiers
156 const preset = frontmatter?.clickAnimation || configs.clickAnimation
157 if (preset)
158 return preset.split(RE_WHITESPACE_OR_COMMA).filter(Boolean)
159 return []
160 })
161
162 const info = ctx.calculate(value)
163 if (!info)
164 return null
165
166 ctx.register(el, info)
167
168 const isShown = computed(() => flagHide ? !info.isActive.value : info.isActive.value)
169 const visibilityState = computed(() => {
170 if (isShown.value)
171 return 'shown'
172 if (Number.isFinite(info.end))
173 return ctx.current < info.start ? 'before' : 'after'
174 else
175 return flagHide ? 'after' : 'before'
176 })
177
178 const resolved = {
179 ...info,
180 isShown,
181 visibilityState,
182 flagHide,
183 flagAnimations,
184 }
185 resolvedClickMap.set(el, resolved)
186 return resolved
187 }
188
189 function unmounted(el: HTMLElement, dir: DirectiveBinding<any>) {
190 el.classList.toggle(CLASS_VCLICK_TARGET, false)
191 const ctx = directiveInject(dir, injectionClicksContext)?.value
192 ctx?.unregister(el)
193 // @ts-expect-error extra prop
194 el.watchStopHandle?.()
195 }
196
196 lines TYPESCRIPT