返回 slidev
v-mark.ts
根目录 / packages / client / modules / v-mark.ts
1 import type { RoughAnnotationConfig } from '@slidev/rough-notation'
2 import type { RawAtValue } from '@slidev/types'
3 import type { App } from 'vue'
4 import { annotate } from '@slidev/rough-notation'
5 import { computed, watchEffect } from 'vue'
6 import { useNav } from '../composables/useNav'
7 import { resolveClick } from './v-click'
8
9 export interface RoughDirectiveOptions extends Partial<RoughAnnotationConfig> {
10 at: RawAtValue
11 }
12
13 export type RoughDirectiveValue = RawAtValue | RoughDirectiveOptions
14
15 function addClass(options: RoughDirectiveOptions, cls: string) {
16 options.class = [options.class, cls].filter(Boolean).join(' ')
17 return options
18 }
19
20 // Definitions of supported modifiers
21 const vMarkModifiers: Record<string, (options: RoughDirectiveOptions, value?: any) => RoughDirectiveOptions> = {
22 // Types
23 'box': options => Object.assign(options, { type: 'box' }),
24 'circle': options => Object.assign(options, { type: 'circle' }),
25 'underline': options => Object.assign(options, { type: 'underline' }),
26 'highlight': options => Object.assign(options, { type: 'highlight' }),
27 'strike-through': options => Object.assign(options, { type: 'strike-through' }),
28 'crossed-off': options => Object.assign(options, { type: 'crossed-off' }),
29 'bracket': options => Object.assign(options, { type: 'bracket' }),
30
31 // Type Aliases
32 'strike': options => Object.assign(options, { type: 'strike-through' }),
33 'cross': options => Object.assign(options, { type: 'crossed-off' }),
34 'crossed': options => Object.assign(options, { type: 'crossed-off' }),
35 'linethrough': options => Object.assign(options, { type: 'strike-through' }),
36 'line-through': options => Object.assign(options, { type: 'strike-through' }),
37
38 // Colors
39 // @unocss-include
40 'black': options => addClass(options, 'text-black'),
41 'blue': options => addClass(options, 'text-blue'),
42 'cyan': options => addClass(options, 'text-cyan'),
43 'gray': options => addClass(options, 'text-gray'),
44 'green': options => addClass(options, 'text-green'),
45 'indigo': options => addClass(options, 'text-indigo'),
46 'lime': options => addClass(options, 'text-lime'),
47 'orange': options => addClass(options, 'text-orange'),
48 'pink': options => addClass(options, 'text-pink'),
49 'purple': options => addClass(options, 'text-purple'),
50 'red': options => addClass(options, 'text-red'),
51 'teal': options => addClass(options, 'text-teal'),
52 'white': options => addClass(options, 'text-white'),
53 'yellow': options => addClass(options, 'text-yellow'),
54 }
55
56 const vMarkModifiersDynamic: [RegExp, (match: RegExpMatchArray, options: RoughDirectiveOptions, value?: any) => RoughDirectiveOptions][] = [
57 // Support setting delay like `v-mark.delay300="1"`
58 [/^delay-?(\d+)?$/, (match, options, value) => {
59 const ms = (match[1] ? Number.parseInt(match[1]) : value) || 300
60 options.delay = ms
61 return options
62 }],
63 // Support setting opacity like `v-mark.op50="1"`
64 [/^(?:op|opacity)-?(\d+)?$/, (match, options, value) => {
65 const opacity = (match[1] ? Number.parseInt(match[1]) : value) || 100
66 options.opacity = opacity / 100
67 return options
68 }],
69 ]
70
71 /**
72 * This supports v-mark directive to add notations to elements, powered by `rough-notation`.
73 */
74 export function createVMarkDirective() {
75 return {
76 install(app: App) {
77 app.directive<HTMLElement, RoughDirectiveValue>('mark', {
78 // @ts-expect-error extra prop
79 name: 'v-mark',
80
81 mounted: (el, binding) => {
82 const { isPrintMode } = useNav()
83
84 const options = computed(() => {
85 const bindingOptions = (typeof binding.value === 'object' && !Array.isArray(binding.value))
86 ? { ...binding.value }
87 : { at: binding.value }
88
89 let modifierOptions: RoughDirectiveOptions = { at: bindingOptions.at }
90 const unknownModifiers = Object.entries(binding.modifiers)
91 .filter(([k, v]) => {
92 if (vMarkModifiers[k]) {
93 modifierOptions = vMarkModifiers[k](modifierOptions, v)
94 return false
95 }
96 for (const [re, fn] of vMarkModifiersDynamic) {
97 const match = k.match(re)
98 if (match) {
99 modifierOptions = fn(match, modifierOptions, v)
100 return false
101 }
102 }
103 return true
104 })
105
106 if (unknownModifiers.length)
107 console.warn('[Slidev] Invalid modifiers for v-mark:', unknownModifiers)
108
109 const options = {
110 ...modifierOptions,
111 ...bindingOptions,
112 }
113 options.type ||= 'underline'
114
115 if (isPrintMode.value)
116 options.animationDuration = 1 /* millisecond */
117
118 return options
119 })
120
121 const annotation = annotate(el, options.value as RoughAnnotationConfig)
122
123 const resolvedClick = resolveClick(el, binding, options.value.at)
124 if (!resolvedClick) {
125 // No click animation, just show the mark
126 annotation.show()
127 return
128 }
129
130 // @ts-expect-error extra prop
131 el.watchStopHandle = watchEffect(() => {
132 let shouldShow: boolean | undefined
133
134 if (options.value.class)
135 annotation.class = options.value.class
136 if (options.value.color)
137 annotation.color = options.value.color
138
139 const at = options.value.at
140
141 if (at === true)
142 shouldShow = true
143 else if (at === false)
144 shouldShow = false
145 else
146 shouldShow = resolvedClick.isActive.value
147
148 if (shouldShow == null)
149 return
150
151 if (shouldShow)
152 annotation.show()
153 else
154 annotation.hide()
155 })
156 },
157
158 unmounted: (el) => {
159 // @ts-expect-error extra prop
160 el.watchStopHandle?.()
161 },
162 })
163 },
164 }
165 }
166
166 lines TYPESCRIPT