feature: vor und nachteile

This commit is contained in:
Hikyu
2026-05-31 13:54:31 +02:00
parent 552a947a1c
commit e3dad17446
23270 changed files with 3024794 additions and 200 deletions
+9
View File
@@ -0,0 +1,9 @@
import type { ComputedRef } from '@vue/reactivity'
import type { SlidevContextNav } from '../composables/useNav'
import type { configs } from '../env'
export interface SlidevContext {
nav: SlidevContextNav
configs: typeof configs
themeConfigs: ComputedRef<typeof configs['themeConfig']>
}
+30
View File
@@ -0,0 +1,30 @@
import { clearUndefined } from '@antfu/utils'
import lz from 'lz-string'
import mermaid from 'mermaid/dist/mermaid.esm.mjs'
import { makeId } from '../logic/utils'
import setupMermaid from '../setup/mermaid'
mermaid.startOnLoad = false
mermaid.initialize({ startOnLoad: false })
const cache = new Map<string, string>()
let containerElement: Element | undefined
export async function renderMermaid(lzEncoded: string, options: any) {
containerElement ??= document.getElementById('mermaid-rendering-container')!
const key = lzEncoded + JSON.stringify(options)
const _cache = cache.get(key)
if (_cache)
return _cache
mermaid.initialize({
startOnLoad: false,
...clearUndefined(await setupMermaid() || {}),
...clearUndefined(options),
})
const code = lz.decompressFromBase64(lzEncoded)
const id = makeId()
const { svg } = await mermaid.render(id, code, containerElement)
cache.set(key, svg)
return svg
}
+161
View File
@@ -0,0 +1,161 @@
import type { ClicksElement, RawAtValue } from '@slidev/types'
import type { App, DirectiveBinding } from 'vue'
import { computed, watchEffect } from 'vue'
import {
CLASS_VCLICK_CURRENT,
CLASS_VCLICK_FADE,
CLASS_VCLICK_HIDDEN,
CLASS_VCLICK_HIDDEN_EXP,
CLASS_VCLICK_PRIOR,
CLASS_VCLICK_TARGET,
injectionClicksContext,
} from '../constants'
import { directiveInject } from '../utils'
export function createVClickDirectives() {
return {
install(app: App) {
app.directive<HTMLElement, RawAtValue>('click', {
// @ts-expect-error extra prop
name: 'v-click',
mounted(el, dir) {
const resolved = resolveClick(el, dir, dir.value)
if (resolved == null)
return
el.classList.toggle(CLASS_VCLICK_TARGET, true)
// Expose the resolved clicks info to the element to make it easier to understand and debug
el.dataset.slidevClicksStart = String(resolved.start)
if (Number.isFinite(resolved.end))
el.dataset.slidevClicksEnd = String(resolved.end)
// @ts-expect-error extra prop
el.watchStopHandle = watchEffect(() => {
const active = resolved.isActive.value
const current = resolved.isCurrent.value
const prior = active && !current
if (resolved.flagHide) {
el.classList.toggle(resolved.flagFade ? CLASS_VCLICK_FADE : CLASS_VCLICK_HIDDEN, active)
el.classList.toggle(CLASS_VCLICK_HIDDEN_EXP, active)
}
else {
el.classList.toggle(resolved.flagFade ? CLASS_VCLICK_FADE : CLASS_VCLICK_HIDDEN, !active)
}
el.classList.toggle(CLASS_VCLICK_CURRENT, current)
el.classList.toggle(CLASS_VCLICK_PRIOR, prior)
})
},
unmounted,
})
app.directive<HTMLElement, RawAtValue>('after', {
// @ts-expect-error extra prop
name: 'v-after',
mounted(el, dir) {
const resolved = resolveClick(el, dir, '+0')
if (resolved == null)
return
el.classList.toggle(CLASS_VCLICK_TARGET, true)
// @ts-expect-error extra prop
el.watchStopHandle = watchEffect(() => {
const active = resolved.isActive.value
const current = resolved.isCurrent.value
const prior = active && !current
if (resolved.flagHide) {
el.classList.toggle(resolved.flagFade ? CLASS_VCLICK_FADE : CLASS_VCLICK_HIDDEN, active)
el.classList.toggle(CLASS_VCLICK_HIDDEN_EXP, active)
}
else {
el.classList.toggle(resolved.flagFade ? CLASS_VCLICK_FADE : CLASS_VCLICK_HIDDEN, !active)
}
el.classList.toggle(CLASS_VCLICK_CURRENT, current)
el.classList.toggle(CLASS_VCLICK_PRIOR, prior)
})
},
unmounted,
})
app.directive<HTMLElement, RawAtValue>('click-hide', {
// @ts-expect-error extra prop
name: 'v-click-hide',
mounted(el, dir) {
const resolved = resolveClick(el, dir, dir.value, true)
if (resolved == null)
return
el.classList.toggle(CLASS_VCLICK_TARGET, true)
// @ts-expect-error extra prop
el.watchStopHandle = watchEffect(() => {
const active = resolved.isActive.value
const current = resolved.isCurrent.value
const prior = active && !current
el.classList.toggle(resolved.flagFade ? CLASS_VCLICK_FADE : CLASS_VCLICK_HIDDEN, active)
el.classList.toggle(CLASS_VCLICK_HIDDEN_EXP, active)
el.classList.toggle(CLASS_VCLICK_CURRENT, current)
el.classList.toggle(CLASS_VCLICK_PRIOR, prior)
})
},
unmounted,
})
},
}
}
export const resolvedClickMap = new Map<ClicksElement, ReturnType<typeof resolveClick>>()
export function resolveClick(el: Element | string, dir: DirectiveBinding<any>, value: RawAtValue, explicitHide = false) {
const ctx = directiveInject(dir, injectionClicksContext)?.value
if (!el || !ctx)
return null
const flagHide = explicitHide || (dir.modifiers.hide !== false && dir.modifiers.hide != null)
const flagFade = dir.modifiers.fade !== false && dir.modifiers.fade != null
const info = ctx.calculate(value)
if (!info)
return null
ctx.register(el, info)
const isShown = computed(() => flagHide ? !info.isActive.value : info.isActive.value)
const visibilityState = computed(() => {
if (isShown.value)
return 'shown'
if (Number.isFinite(info.end))
return ctx.current < info.start ? 'before' : 'after'
else
return flagHide ? 'after' : 'before'
})
const resolved = {
...info,
isShown,
visibilityState,
flagFade,
flagHide,
}
resolvedClickMap.set(el, resolved)
return resolved
}
function unmounted(el: HTMLElement, dir: DirectiveBinding<any>) {
el.classList.toggle(CLASS_VCLICK_TARGET, false)
const ctx = directiveInject(dir, injectionClicksContext)?.value
ctx?.unregister(el)
// @ts-expect-error extra prop
el.watchStopHandle?.()
}
+44
View File
@@ -0,0 +1,44 @@
import type { App } from 'vue'
import type { DragElementState } from '../composables/useDragElements'
import { watch } from 'vue'
import { useDragElement } from '../composables/useDragElements'
export function createVDragDirective() {
return {
install(app: App) {
app.directive<HTMLElement & { draggingState: DragElementState }>('drag', {
// @ts-expect-error extra prop
name: 'v-drag',
created(el, binding, vnode) {
const state = useDragElement(binding, binding.value, vnode.props?.markdownSource)
if (vnode.props) {
vnode.props = { ...vnode.props }
delete vnode.props.markdownSource
}
state.container.value = el
el.draggingState = state
el.dataset.dragId = state.dragId
state.watchStopHandles.push(
watch(state.containerStyle, (style) => {
for (const [k, v] of Object.entries(style)) {
if (v)
el.style[k as any] = v as any
}
}, { immediate: true }),
)
el.addEventListener('dblclick', state.startDragging)
},
mounted(el) {
el.draggingState.mounted()
},
unmounted(el) {
const state = el.draggingState
state.unmounted()
el.removeEventListener('dblclick', state.startDragging)
state.watchStopHandles.forEach(fn => fn())
},
})
},
}
}
+165
View File
@@ -0,0 +1,165 @@
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<RoughAnnotationConfig> {
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<string, (options: RoughDirectiveOptions, value?: any) => 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<HTMLElement, RoughDirectiveValue>('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?.()
},
})
},
}
}
+109
View File
@@ -0,0 +1,109 @@
import type { ClicksInfo } from '@slidev/types'
import type { App, ObjectDirective } from 'vue'
import { MotionDirective } from '@vueuse/motion'
import { watch } from 'vue'
import { useNav } from '../composables/useNav'
import { injectionClicksContext, injectionCurrentPage, injectionRenderContext } from '../constants'
import { makeId } from '../logic/utils'
import { directiveInject } from '../utils'
import { resolvedClickMap } from './v-click'
export function createVMotionDirectives() {
return {
install(app: App) {
const original = MotionDirective() as ObjectDirective
app.directive<HTMLElement | SVGElement, string>('motion', {
// @ts-expect-error extra prop
name: 'v-motion',
mounted(el, binding, node, prevNode) {
const clicksContext = directiveInject(binding, injectionClicksContext)
const thisPage = directiveInject(binding, injectionCurrentPage)
const renderContext = directiveInject(binding, injectionRenderContext)
const { currentPage, clicks: currentClicks, isPrintMode } = useNav()
const props = node.props = { ...node.props }
const variantInitial = { ...props.initial, ...props.variants?.['slidev-initial'] }
const variantEnter = { ...props.enter, ...props.variants?.['slidev-enter'] }
const variantLeave = { ...props.leave, ...props.variants?.['slidev-leave'] }
delete props.initial
delete props.enter
delete props.leave
const idPrefix = `${makeId()}-`
const clicks: {
id: string
at: number | [number, number]
variant: Record<string, unknown>
info: ClicksInfo | null | undefined
}[] = []
for (const k of Object.keys(props)) {
if (k.startsWith('click-')) {
const s = k.slice(6)
const at = s.includes('-') ? s.split('-').map(Number) as [number, number] : +s
const id = idPrefix + s
clicks.push({
id,
at,
variant: { ...props[k] },
info: clicksContext?.value.calculate(at),
})
delete props[k]
}
}
clicks.sort((a, b) => (Array.isArray(a.at) ? a.at[0] : a.at) - (Array.isArray(b.at) ? b.at[0] : b.at))
original.created!(el, binding, node, prevNode)
original.mounted!(el, binding, node, prevNode)
// @ts-expect-error extra prop
const motion = el.motionInstance
motion.clickIds = clicks.map(i => i.id)
motion.set(variantInitial)
motion.watchStopHandle = watch(
[thisPage, currentPage, currentClicks].filter(Boolean),
() => {
const visibility = resolvedClickMap.get(el)?.visibilityState.value ?? 'shown'
if (!clicksContext?.value || !['slide', 'presenter'].includes(renderContext?.value ?? '')) {
const mixedVariant: Record<string, unknown> = { ...variantInitial, ...variantEnter }
for (const { variant } of clicks)
Object.assign(mixedVariant, variant)
motion.set(mixedVariant)
}
else if (isPrintMode.value || thisPage?.value === currentPage.value) {
if (visibility === 'shown') {
const mixedVariant: Record<string, unknown> = { ...variantInitial, ...variantEnter }
for (const { variant, info } of clicks) {
if (!info || info.isActive.value)
Object.assign(mixedVariant, variant)
}
if (isPrintMode.value)
motion.set(mixedVariant) // print with clicks
else
motion.apply(mixedVariant)
}
else {
motion.apply(visibility === 'before' ? variantInitial : variantLeave)
}
}
else {
motion.apply((thisPage?.value ?? -1) > currentPage.value ? variantInitial : variantLeave)
}
},
{
immediate: true,
},
)
},
unmounted(el) {
// @ts-expect-error extra prop
const motion = el.motionInstance
motion.watchStopHandle()
},
})
},
}
}