import type { RoughAnnotationConfig } from '@slidev/rough-notation' import type { RawAtValue } from '@slidev/types' import type { App } from 'vue' import { annotate } from '@slidev/rough-notation' import { computed, watchEffect } from 'vue' import { useNav } from '../composables/useNav' import { resolveClick } from './v-click' export interface RoughDirectiveOptions extends Partial { at: RawAtValue } export type RoughDirectiveValue = RawAtValue | RoughDirectiveOptions function addClass(options: RoughDirectiveOptions, cls: string) { options.class = [options.class, cls].filter(Boolean).join(' ') return options } // Definitions of supported modifiers const vMarkModifiers: Record RoughDirectiveOptions> = { // Types 'box': options => Object.assign(options, { type: 'box' }), 'circle': options => Object.assign(options, { type: 'circle' }), 'underline': options => Object.assign(options, { type: 'underline' }), 'highlight': options => Object.assign(options, { type: 'highlight' }), 'strike-through': options => Object.assign(options, { type: 'strike-through' }), 'crossed-off': options => Object.assign(options, { type: 'crossed-off' }), 'bracket': options => Object.assign(options, { type: 'bracket' }), // Type Aliases 'strike': options => Object.assign(options, { type: 'strike-through' }), 'cross': options => Object.assign(options, { type: 'crossed-off' }), 'crossed': options => Object.assign(options, { type: 'crossed-off' }), 'linethrough': options => Object.assign(options, { type: 'strike-through' }), 'line-through': options => Object.assign(options, { type: 'strike-through' }), // Colors // @unocss-include 'black': options => addClass(options, 'text-black'), 'blue': options => addClass(options, 'text-blue'), 'cyan': options => addClass(options, 'text-cyan'), 'gray': options => addClass(options, 'text-gray'), 'green': options => addClass(options, 'text-green'), 'indigo': options => addClass(options, 'text-indigo'), 'lime': options => addClass(options, 'text-lime'), 'orange': options => addClass(options, 'text-orange'), 'pink': options => addClass(options, 'text-pink'), 'purple': options => addClass(options, 'text-purple'), 'red': options => addClass(options, 'text-red'), 'teal': options => addClass(options, 'text-teal'), 'white': options => addClass(options, 'text-white'), 'yellow': options => addClass(options, 'text-yellow'), } const vMarkModifiersDynamic: [RegExp, (match: RegExpMatchArray, options: RoughDirectiveOptions, value?: any) => RoughDirectiveOptions][] = [ // Support setting delay like `v-mark.delay300="1"` [/^delay-?(\d+)?$/, (match, options, value) => { const ms = (match[1] ? Number.parseInt(match[1]) : value) || 300 options.delay = ms return options }], // Support setting opacity like `v-mark.op50="1"` [/^(?:op|opacity)-?(\d+)?$/, (match, options, value) => { const opacity = (match[1] ? Number.parseInt(match[1]) : value) || 100 options.opacity = opacity / 100 return options }], ] /** * This supports v-mark directive to add notations to elements, powered by `rough-notation`. */ export function createVMarkDirective() { return { install(app: App) { app.directive('mark', { // @ts-expect-error extra prop name: 'v-mark', mounted: (el, binding) => { const { isPrintMode } = useNav() const options = computed(() => { const bindingOptions = (typeof binding.value === 'object' && !Array.isArray(binding.value)) ? { ...binding.value } : { at: binding.value } let modifierOptions: RoughDirectiveOptions = { at: bindingOptions.at } const unknownModifiers = Object.entries(binding.modifiers) .filter(([k, v]) => { if (vMarkModifiers[k]) { modifierOptions = vMarkModifiers[k](modifierOptions, v) return false } for (const [re, fn] of vMarkModifiersDynamic) { const match = k.match(re) if (match) { modifierOptions = fn(match, modifierOptions, v) return false } } return true }) if (unknownModifiers.length) console.warn('[Slidev] Invalid modifiers for v-mark:', unknownModifiers) const options = { ...modifierOptions, ...bindingOptions, } options.type ||= 'underline' if (isPrintMode.value) options.animationDuration = 1 /* millisecond */ return options }) const annotation = annotate(el, options.value as RoughAnnotationConfig) const resolvedClick = resolveClick(el, binding, options.value.at) if (!resolvedClick) { // No click animation, just show the mark annotation.show() return } // @ts-expect-error extra prop el.watchStopHandle = watchEffect(() => { let shouldShow: boolean | undefined if (options.value.class) annotation.class = options.value.class if (options.value.color) annotation.color = options.value.color const at = options.value.at if (at === true) shouldShow = true else if (at === false) shouldShow = false else shouldShow = resolvedClick.isActive.value if (shouldShow == null) return if (shouldShow) annotation.show() else annotation.hide() }) }, unmounted: (el) => { // @ts-expect-error extra prop el.watchStopHandle?.() }, }) }, } }