@slidev/client 0.48.7 → 0.48.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -45,43 +45,45 @@ onMounted(() => {
45
45
  // Calculate the step and rangeStr based on the current click count
46
46
  const clickCount = clicks.current - start
47
47
  let step = steps.length - 1
48
- let _currentClickSum = 0
48
+ let currentClickSum = 0
49
49
  let rangeStr = 'all'
50
50
  for (let i = 0; i < ranges.value.length; i++) {
51
51
  const current = ranges.value[i]
52
- if (clickCount < _currentClickSum + current.length - 1) {
52
+ if (clickCount < currentClickSum + current.length - 1) {
53
53
  step = i
54
- rangeStr = current[clickCount - _currentClickSum + 1]
54
+ rangeStr = current[clickCount - currentClickSum + 1]
55
55
  break
56
56
  }
57
- _currentClickSum += current.length || 1
57
+ currentClickSum += current.length || 1
58
58
  }
59
59
  stepIndex.value = step
60
60
 
61
- const pre = container.value?.querySelector('.shiki') as HTMLElement
62
- if (!pre)
63
- return
61
+ setTimeout(() => {
62
+ const pre = container.value?.querySelector('.shiki') as HTMLElement
63
+ if (!pre)
64
+ return
64
65
 
65
- const children = (Array.from(pre.children) as HTMLElement[])
66
- .slice(1) // Remove the first anchor
67
- .filter(i => !i.className.includes('shiki-magic-move-leave')) // Filter the leaving elements
66
+ const children = (Array.from(pre.children) as HTMLElement[])
67
+ .slice(1) // Remove the first anchor
68
+ .filter(i => !i.className.includes('shiki-magic-move-leave')) // Filter the leaving elements
68
69
 
69
- // Group to lines between `<br>`
70
- const lines = children.reduce((acc, el) => {
71
- if (el.tagName === 'BR')
72
- acc.push([])
73
- else
74
- acc[acc.length - 1].push(el)
75
- return acc
76
- }, [[]] as HTMLElement[][])
70
+ // Group to lines between `<br>`
71
+ const lines = children.reduce((acc, el) => {
72
+ if (el.tagName === 'BR')
73
+ acc.push([])
74
+ else
75
+ acc[acc.length - 1].push(el)
76
+ return acc
77
+ }, [[]] as HTMLElement[][])
77
78
 
78
- // Update highlight range
79
- updateCodeHighlightRange(
80
- rangeStr,
81
- lines.length,
82
- 1,
83
- no => lines[no],
84
- )
79
+ // Update highlight range
80
+ updateCodeHighlightRange(
81
+ rangeStr,
82
+ lines.length,
83
+ 1,
84
+ no => lines[no],
85
+ )
86
+ })
85
87
  },
86
88
  { immediate: true },
87
89
  )
@@ -104,3 +106,10 @@ onMounted(() => {
104
106
  />
105
107
  </div>
106
108
  </template>
109
+
110
+ <style>
111
+ .slidev-code-magic-move .shiki-magic-move-enter-from,
112
+ .slidev-code-magic-move .shiki-magic-move-leave-to {
113
+ opacity: 0;
114
+ }
115
+ </style>
@@ -1,4 +1,4 @@
1
- import { sum } from '@antfu/utils'
1
+ import { clamp, sum } from '@antfu/utils'
2
2
  import type { ClicksContext, SlideRoute } from '@slidev/types'
3
3
  import type { Ref } from 'vue'
4
4
  import { ref, shallowReactive } from 'vue'
@@ -7,18 +7,21 @@ import { routeForceRefresh } from '../logic/route'
7
7
 
8
8
  export function createClicksContextBase(
9
9
  current: Ref<number>,
10
- clicksOverrides?: number,
10
+ clicksStart = 0,
11
+ clicksTotalOverrides?: number,
11
12
  ): ClicksContext {
12
13
  const relativeOffsets: ClicksContext['relativeOffsets'] = new Map()
13
14
  const map: ClicksContext['map'] = shallowReactive(new Map())
14
15
 
15
16
  return {
16
17
  get current() {
17
- return +current.value
18
+ // Here we haven't know clicksTotal yet.
19
+ return clamp(+current.value, clicksStart, this.total)
18
20
  },
19
21
  set current(value) {
20
- current.value = +value
22
+ current.value = clamp(+value, clicksStart, this.total)
21
23
  },
24
+ clicksStart,
22
25
  relativeOffsets,
23
26
  map,
24
27
  onMounted() { },
@@ -56,7 +59,7 @@ export function createClicksContextBase(
56
59
  get total() {
57
60
  // eslint-disable-next-line no-unused-expressions
58
61
  routeForceRefresh.value
59
- return clicksOverrides ?? Math.max(0, ...[...map.values()].map(v => v.max || 0))
62
+ return clicksTotalOverrides ?? Math.max(0, ...[...map.values()].map(v => v.max || 0))
60
63
  },
61
64
  }
62
65
  }
@@ -65,5 +68,10 @@ export function createFixedClicks(
65
68
  route?: SlideRoute | undefined,
66
69
  currentInit = 0,
67
70
  ): ClicksContext {
68
- return createClicksContextBase(ref(currentInit), route?.meta?.clicks)
71
+ const clicksStart = route?.meta.slide?.frontmatter.clicksStart ?? 0
72
+ return createClicksContextBase(
73
+ ref(Math.max(currentInit, clicksStart)),
74
+ clicksStart,
75
+ route?.meta?.clicks,
76
+ )
69
77
  }
@@ -5,6 +5,7 @@ import { useRouter } from 'vue-router'
5
5
  import type { RouteLocationNormalized, Router } from 'vue-router'
6
6
  import { createSharedComposable } from '@vueuse/core'
7
7
  import { logicOr } from '@vueuse/math'
8
+ import { clamp } from '@antfu/utils'
8
9
  import { getCurrentTransition } from '../logic/transition'
9
10
  import { getSlide, getSlidePath } from '../logic/slides'
10
11
  import { CLICKS_MAX } from '../constants'
@@ -33,6 +34,7 @@ export interface SlidevContextNav {
33
34
 
34
35
  clicksContext: ComputedRef<ClicksContext>
35
36
  clicks: ComputedRef<number>
37
+ clicksStart: ComputedRef<number>
36
38
  clicksTotal: ComputedRef<number>
37
39
 
38
40
  /** The table of content tree */
@@ -99,6 +101,7 @@ export function useNavBase(
99
101
  const currentLayout = computed(() => currentSlideRoute.value.meta?.layout || (currentSlideNo.value === 1 ? 'cover' : 'default'))
100
102
 
101
103
  const clicks = computed(() => clicksContext.value.current)
104
+ const clicksStart = computed(() => clicksContext.value.clicksStart)
102
105
  const clicksTotal = computed(() => clicksContext.value.total)
103
106
  const nextRoute = computed(() => slides.value[Math.min(slides.value.length, currentSlideNo.value + 1) - 1])
104
107
  const prevRoute = computed(() => slides.value[Math.max(1, currentSlideNo.value - 1) - 1])
@@ -140,7 +143,7 @@ export function useNavBase(
140
143
 
141
144
  async function prev() {
142
145
  clicksDirection.value = -1
143
- if (queryClicks.value <= 0)
146
+ if (queryClicks.value <= clicksStart.value)
144
147
  await prevSlide()
145
148
  else
146
149
  queryClicks.value -= 1
@@ -177,6 +180,9 @@ export function useNavBase(
177
180
  skipTransition.value = false
178
181
  const pageChanged = currentSlideNo.value !== page
179
182
  const clicksChanged = clicks !== queryClicks.value
183
+ const meta = getSlide(page)?.meta
184
+ const clicksStart = meta?.slide?.frontmatter.clicksStart ?? 0
185
+ clicks = clamp(clicks, clicksStart, meta?.__clicksContext?.total ?? CLICKS_MAX)
180
186
  if (pageChanged || clicksChanged) {
181
187
  await router?.push({
182
188
  path: getSlidePath(page, isPresenter.value),
@@ -202,6 +208,7 @@ export function useNavBase(
202
208
  prevRoute,
203
209
  clicksContext,
204
210
  clicks,
211
+ clicksStart,
205
212
  clicksTotal,
206
213
  hasNext,
207
214
  hasPrev,
@@ -289,24 +296,25 @@ const useNavState = createSharedComposable((): SlidevContextNavState => {
289
296
  computed({
290
297
  get() {
291
298
  if (currentSlideNo.value === thisNo)
292
- return +(queryClicksRaw.value || 0) || 0
299
+ return Math.max(+(queryClicksRaw.value ?? 0), context.clicksStart)
293
300
  else if (currentSlideNo.value > thisNo)
294
301
  return CLICKS_MAX
295
302
  else
296
- return 0
303
+ return context.clicksStart
297
304
  },
298
305
  set(v) {
299
306
  if (currentSlideNo.value === thisNo)
300
- queryClicksRaw.value = Math.min(v, context.total).toString()
307
+ queryClicksRaw.value = clamp(v, context.clicksStart, context.total).toString()
301
308
  },
302
309
  }),
303
- route?.meta?.clicks,
310
+ route?.meta.slide?.frontmatter.clicksStart ?? 0,
311
+ route?.meta.clicks,
304
312
  )
305
313
 
306
314
  // On slide mounted, make sure the query is not greater than the total
307
315
  context.onMounted = () => {
308
- if (queryClicksRaw.value)
309
- queryClicksRaw.value = Math.min(+queryClicksRaw.value, context.total).toString()
316
+ if (currentSlideNo.value === thisNo)
317
+ queryClicksRaw.value = clamp(+queryClicksRaw.value, context.clicksStart, context.total).toString()
310
318
  }
311
319
 
312
320
  if (route?.meta)
package/constants.ts CHANGED
@@ -13,6 +13,7 @@ export const injectionRenderContext = '$$slidev-render-context' as unknown as In
13
13
  export const injectionActive = '$$slidev-active' as unknown as InjectionKey<Ref<boolean>>
14
14
  export const injectionFrontmatter = '$$slidev-fontmatter' as unknown as InjectionKey<Record<string, any>>
15
15
  export const injectionSlideZoom = '$$slidev-slide-zoom' as unknown as InjectionKey<ComputedRef<number>>
16
+ export const injectionClickVisibility = '$$slidev-click-visibility' as unknown as InjectionKey<ComputedRef<true | 'before' | 'after'>>
16
17
 
17
18
  export const CLASS_VCLICK_TARGET = 'slidev-vclick-target'
18
19
  export const CLASS_VCLICK_HIDDEN = 'slidev-vclick-hidden'
@@ -31,6 +32,7 @@ export const TRUST_ORIGINS = [
31
32
 
32
33
  export const FRONTMATTER_FIELDS = [
33
34
  'clicks',
35
+ 'clicksStart',
34
36
  'disabled',
35
37
  'hide',
36
38
  'hideInToc',
@@ -1,5 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import type { ClicksContext } from '@slidev/types'
3
+ import { clamp, range } from '@antfu/utils'
3
4
  import { computed } from 'vue'
4
5
  import { CLICKS_MAX } from '../constants'
5
6
 
@@ -8,6 +9,8 @@ const props = defineProps<{
8
9
  }>()
9
10
 
10
11
  const total = computed(() => props.clicksContext.total)
12
+ const start = computed(() => clamp(0, props.clicksContext.clicksStart, total.value))
13
+ const length = computed(() => total.value - start.value + 1)
11
14
  const current = computed({
12
15
  get() {
13
16
  return props.clicksContext.current > total.value ? -1 : props.clicksContext.current
@@ -18,7 +21,7 @@ const current = computed({
18
21
  },
19
22
  })
20
23
 
21
- const range = computed(() => Array.from({ length: total.value + 1 }, (_, i) => i))
24
+ const clicksRange = computed(() => range(start.value, total.value + 1))
22
25
 
23
26
  function onMousedown() {
24
27
  if (current.value < 0 || current.value > total.value)
@@ -29,8 +32,8 @@ function onMousedown() {
29
32
  <template>
30
33
  <div
31
34
  class="flex gap-1 items-center select-none"
32
- :title="`Clicks in this slide: ${total}`"
33
- :class="total ? '' : 'op50'"
35
+ :title="`Clicks in this slide: ${length}`"
36
+ :class="length ? '' : 'op50'"
34
37
  >
35
38
  <div class="flex gap-0.5 items-center min-w-16 font-mono mr1">
36
39
  <carbon:cursor-1 text-sm op50 />
@@ -46,13 +49,13 @@ function onMousedown() {
46
49
  @dblclick="current = clicksContext.total"
47
50
  >
48
51
  <div
49
- v-for="i of range" :key="i"
52
+ v-for="i of clicksRange" :key="i"
50
53
  border="y main" of-hidden relative
51
54
  :class="[
52
55
  i === 0 ? 'rounded-l border-l' : '',
53
56
  i === total ? 'rounded-r border-r' : '',
54
57
  ]"
55
- :style="{ width: total > 0 ? `${1 / total * 100}%` : '100%' }"
58
+ :style="{ width: length > 0 ? `${1 / length * 100}%` : '100%' }"
56
59
  >
57
60
  <div absolute inset-0 :class="i <= current ? 'bg-primary op15' : ''" />
58
61
  <div
@@ -69,8 +72,8 @@ function onMousedown() {
69
72
  <input
70
73
  v-model="current"
71
74
  class="range" absolute inset-0
72
- type="range" :min="0" :max="total" :step="1" z-10 op0
73
- :style="{ '--thumb-width': `${1 / (total + 1) * 100}%` }"
75
+ type="range" :min="start" :max="total" :step="1" z-10 op0
76
+ :style="{ '--thumb-width': `${1 / (length + 1) * 100}%` }"
74
77
  @mousedown="onMousedown"
75
78
  @focus="event => (event.currentTarget as HTMLElement)?.blur()"
76
79
  >
@@ -3,7 +3,7 @@ import { computed, ref, watch } from 'vue'
3
3
  import Fuse from 'fuse.js'
4
4
  import { activeElement, showGotoDialog } from '../state'
5
5
  import { useNav } from '../composables/useNav'
6
- import Titles from '#slidev/title-renderer'
6
+ import TitleRenderer from '#slidev/title-renderer'
7
7
 
8
8
  const container = ref<HTMLDivElement>()
9
9
  const input = ref<HTMLInputElement>()
@@ -161,7 +161,7 @@ watch(activeElement, () => {
161
161
  <div w-4 text-right op50 text-sm>
162
162
  {{ item.no }}
163
163
  </div>
164
- <Titles :no="item.no" />
164
+ <TitleRenderer :no="item.no" />
165
165
  </li>
166
166
  </ul>
167
167
  </div>
@@ -8,8 +8,12 @@ import { sharedState } from '../state/shared'
8
8
  class="absolute top-0 left-0 right-0 bottom-0 pointer-events-none text-xl"
9
9
  >
10
10
  <ph-cursor-fill
11
- class="absolute"
12
- :style="{ left: `${sharedState.cursor.x}%`, top: `${sharedState.cursor.y}%` }"
11
+ class="absolute stroke-white dark:stroke-black"
12
+ :style="{
13
+ left: `${sharedState.cursor.x}%`,
14
+ top: `${sharedState.cursor.y}%`,
15
+ strokeWidth: 16,
16
+ }"
13
17
  />
14
18
  </div>
15
19
  </template>
@@ -16,10 +16,14 @@ const clicks0 = createFixedClicks(route, isPrintWithClicks.value ? 0 : CLICKS_MA
16
16
  :nav="useFixedNav(route, clicks0)"
17
17
  />
18
18
  <template v-if="isPrintWithClicks">
19
+ <!--
20
+ clicks0.total can be any number >=0 when rendering.
21
+ So total-clicksStart can be negative in intermediate states.
22
+ -->
19
23
  <PrintSlideClick
20
- v-for="i of clicks0.total"
24
+ v-for="i in Math.max(0, clicks0.total - clicks0.clicksStart)"
21
25
  :key="i"
22
- :nav="useFixedNav(route, createFixedClicks(route, i))"
26
+ :nav="useFixedNav(route, createFixedClicks(route, i + clicks0.clicksStart))"
23
27
  />
24
28
  </template>
25
29
  </template>
@@ -76,7 +76,6 @@ function onAfterLeave() {
76
76
  :class="getSlideClass(route)"
77
77
  :route="route"
78
78
  :render-context="renderContext"
79
- class="overflow-hidden"
80
79
  />
81
80
  </div>
82
81
  </component>
@@ -1,5 +1,5 @@
1
1
  import type { ResolvedClicksInfo } from '@slidev/types'
2
- import type { App, DirectiveBinding, InjectionKey } from 'vue'
2
+ import type { App, DirectiveBinding } from 'vue'
3
3
  import { computed, watchEffect } from 'vue'
4
4
  import {
5
5
  CLASS_VCLICK_CURRENT,
@@ -8,14 +8,12 @@ import {
8
8
  CLASS_VCLICK_HIDDEN_EXP,
9
9
  CLASS_VCLICK_PRIOR,
10
10
  CLASS_VCLICK_TARGET,
11
+ injectionClickVisibility,
11
12
  injectionClicksContext,
12
13
  } from '../constants'
14
+ import { directiveInject, directiveProvide } from '../utils'
13
15
 
14
- export type VClickValue = string | [string | number, string | number] | boolean
15
-
16
- export function dirInject<T = unknown>(dir: DirectiveBinding<any>, key: InjectionKey<T> | string, defaultValue?: T): T | undefined {
17
- return (dir.instance?.$ as any).provides[key as any] ?? defaultValue
18
- }
16
+ export type VClickValue = undefined | string | number | [string | number, string | number] | boolean
19
17
 
20
18
  export function createVClickDirectives() {
21
19
  return {
@@ -25,7 +23,7 @@ export function createVClickDirectives() {
25
23
  name: 'v-click',
26
24
 
27
25
  mounted(el, dir) {
28
- const resolved = resolveClick(el, dir, dir.value)
26
+ const resolved = resolveClick(el, dir, dir.value, true)
29
27
  if (resolved == null)
30
28
  return
31
29
 
@@ -37,7 +35,8 @@ export function createVClickDirectives() {
37
35
  if (clicks[1] != null)
38
36
  el.dataset.slidevClicksEnd = String(clicks[1])
39
37
 
40
- watchEffect(() => {
38
+ // @ts-expect-error extra prop
39
+ el.watchStopHandle = watchEffect(() => {
41
40
  const active = resolved.isActive.value
42
41
  const current = resolved.isCurrent.value
43
42
  const prior = active && !current
@@ -62,13 +61,14 @@ export function createVClickDirectives() {
62
61
  name: 'v-after',
63
62
 
64
63
  mounted(el, dir) {
65
- const resolved = resolveClick(el, dir, dir.value, true)
64
+ const resolved = resolveClick(el, dir, dir.value, true, true)
66
65
  if (resolved == null)
67
66
  return
68
67
 
69
68
  el.classList.toggle(CLASS_VCLICK_TARGET, true)
70
69
 
71
- watchEffect(() => {
70
+ // @ts-expect-error extra prop
71
+ el.watchStopHandle = watchEffect(() => {
72
72
  const active = resolved.isActive.value
73
73
  const current = resolved.isCurrent.value
74
74
  const prior = active && !current
@@ -93,13 +93,14 @@ export function createVClickDirectives() {
93
93
  name: 'v-click-hide',
94
94
 
95
95
  mounted(el, dir) {
96
- const resolved = resolveClick(el, dir, dir.value, false, true)
96
+ const resolved = resolveClick(el, dir, dir.value, true, false, true)
97
97
  if (resolved == null)
98
98
  return
99
99
 
100
100
  el.classList.toggle(CLASS_VCLICK_TARGET, true)
101
101
 
102
- watchEffect(() => {
102
+ // @ts-expect-error extra prop
103
+ el.watchStopHandle = watchEffect(() => {
103
104
  const active = resolved.isActive.value
104
105
  const current = resolved.isCurrent.value
105
106
  const prior = active && !current
@@ -117,20 +118,20 @@ export function createVClickDirectives() {
117
118
  }
118
119
  }
119
120
 
120
- function isActive(thisClick: number | [number, number], clicks: number) {
121
+ function isClickActive(thisClick: number | [number, number], clicks: number) {
121
122
  return Array.isArray(thisClick)
122
123
  ? thisClick[0] <= clicks && clicks < thisClick[1]
123
124
  : thisClick <= clicks
124
125
  }
125
126
 
126
- function isCurrent(thisClick: number | [number, number], clicks: number) {
127
+ function isClickCurrent(thisClick: number | [number, number], clicks: number) {
127
128
  return Array.isArray(thisClick)
128
129
  ? thisClick[0] === clicks
129
130
  : thisClick === clicks
130
131
  }
131
132
 
132
- export function resolveClick(el: Element, dir: DirectiveBinding<any>, value: VClickValue, clickAfter = false, flagHide = false): ResolvedClicksInfo | null {
133
- const ctx = dirInject(dir, injectionClicksContext)?.value
133
+ export function resolveClick(el: Element | string, dir: DirectiveBinding<any>, value: VClickValue, provideVisibility = false, clickAfter = false, flagHide = false): ResolvedClicksInfo | null {
134
+ const ctx = directiveInject(dir, injectionClicksContext)?.value
134
135
 
135
136
  if (!el || !ctx)
136
137
  return null
@@ -152,29 +153,47 @@ export function resolveClick(el: Element, dir: DirectiveBinding<any>, value: VCl
152
153
  if (Array.isArray(value)) {
153
154
  // range (absolute)
154
155
  delta = 0
155
- thisClick = value as [number, number]
156
+ thisClick = [+value[0], +value[1]]
156
157
  maxClick = +value[1]
157
158
  }
158
159
  else {
159
160
  ({ start: thisClick, end: maxClick, delta } = ctx.resolve(value))
160
161
  }
161
162
 
163
+ const isActive = computed(() => isClickActive(thisClick, ctx.current))
164
+ const isCurrent = computed(() => isClickCurrent(thisClick, ctx.current))
165
+ const isShown = computed(() => flagHide ? !isActive.value : isActive.value)
166
+
162
167
  const resolved: ResolvedClicksInfo = {
163
168
  max: maxClick,
164
169
  clicks: thisClick,
165
170
  delta,
166
- isActive: computed(() => isActive(thisClick, ctx.current)),
167
- isCurrent: computed(() => isCurrent(thisClick, ctx.current)),
168
- isShown: computed(() => flagHide ? !isActive(thisClick, ctx.current) : isActive(thisClick, ctx.current)),
171
+ isActive,
172
+ isCurrent,
173
+ isShown,
169
174
  flagFade,
170
175
  flagHide,
171
176
  }
172
177
  ctx.register(el, resolved)
178
+
179
+ if (provideVisibility) {
180
+ directiveProvide(dir, injectionClickVisibility, computed(() => {
181
+ if (isShown.value)
182
+ return true
183
+ if (Array.isArray(thisClick))
184
+ return ctx.current < thisClick[0] ? 'before' : 'after'
185
+ else
186
+ return flagHide ? 'after' : 'before'
187
+ }))
188
+ }
189
+
173
190
  return resolved
174
191
  }
175
192
 
176
193
  function unmounted(el: HTMLElement, dir: DirectiveBinding<any>) {
177
194
  el.classList.toggle(CLASS_VCLICK_TARGET, false)
178
- const ctx = dirInject(dir, injectionClicksContext)?.value
195
+ const ctx = directiveInject(dir, injectionClicksContext)?.value
179
196
  ctx?.unregister(el)
197
+ // @ts-expect-error extra prop
198
+ el.watchStopHandle?.()
180
199
  }
package/modules/v-mark.ts CHANGED
@@ -121,7 +121,8 @@ export function createVMarkDirective() {
121
121
  return
122
122
  }
123
123
 
124
- watchEffect(() => {
124
+ // @ts-expect-error extra prop
125
+ el.watchStopHandle = watchEffect(() => {
125
126
  let shouldShow: boolean | undefined
126
127
 
127
128
  if (options.value.class)
@@ -147,6 +148,11 @@ export function createVMarkDirective() {
147
148
  annotation.hide()
148
149
  })
149
150
  },
151
+
152
+ unmounted: (el) => {
153
+ // @ts-expect-error extra prop
154
+ el.watchStopHandle?.()
155
+ },
150
156
  })
151
157
  },
152
158
  }
@@ -0,0 +1,120 @@
1
+ import type { App, ObjectDirective } from 'vue'
2
+ import { watch } from 'vue'
3
+ import { MotionDirective } from '@vueuse/motion'
4
+ import type { ResolvedClicksInfo } from '@slidev/types'
5
+ import { injectionClickVisibility, injectionClicksContext, injectionCurrentPage, injectionRenderContext } from '../constants'
6
+ import { useNav } from '../composables/useNav'
7
+ import { makeId } from '../logic/utils'
8
+ import { directiveInject } from '../utils'
9
+ import type { VClickValue } from './v-click'
10
+ import { resolveClick } from './v-click'
11
+
12
+ export type MotionDirectiveValue = undefined | VClickValue | {
13
+ key?: string
14
+ at?: VClickValue
15
+ }
16
+
17
+ export function createVMotionDirectives() {
18
+ return {
19
+ install(app: App) {
20
+ const original = MotionDirective() as ObjectDirective
21
+ app.directive<HTMLElement | SVGElement, string>('motion', {
22
+ // @ts-expect-error extra prop
23
+ name: 'v-motion',
24
+ mounted(el, binding, node, prevNode) {
25
+ const props = node.props = { ...node.props }
26
+
27
+ const variantInitial = { ...props.initial, ...props.variants?.['slidev-initial'] }
28
+ const variantEnter = { ...props.enter, ...props.variants?.['slidev-enter'] }
29
+ const variantLeave = { ...props.leave, ...props.variants?.['slidev-leave'] }
30
+ delete props.initial
31
+ delete props.enter
32
+ delete props.leave
33
+
34
+ const idPrefix = `${makeId()}-`
35
+ const clicks: {
36
+ id: string
37
+ at: number | [number, number]
38
+ variant: Record<string, unknown>
39
+ resolved: ResolvedClicksInfo | null
40
+ }[] = []
41
+
42
+ for (const k of Object.keys(props)) {
43
+ if (k.startsWith('click-')) {
44
+ const s = k.slice(6)
45
+ const at = s.includes('-') ? s.split('-').map(Number) as [number, number] : +s
46
+ const id = idPrefix + s
47
+ clicks.push({
48
+ id,
49
+ at,
50
+ variant: { ...props[k] },
51
+ resolved: resolveClick(id, binding, at),
52
+ })
53
+ delete props[k]
54
+ }
55
+ }
56
+
57
+ clicks.sort((a, b) => (Array.isArray(a.at) ? a.at[0] : a.at) - (Array.isArray(b.at) ? b.at[0] : b.at))
58
+
59
+ original.created!(el, binding, node, prevNode)
60
+ original.mounted!(el, binding, node, prevNode)
61
+
62
+ const thisPage = directiveInject(binding, injectionCurrentPage)
63
+ const renderContext = directiveInject(binding, injectionRenderContext)
64
+ const clickVisibility = directiveInject(binding, injectionClickVisibility)
65
+ const clicksContext = directiveInject(binding, injectionClicksContext)
66
+ const { currentPage, clicks: currentClicks, isPrintMode } = useNav()
67
+ // @ts-expect-error extra prop
68
+ const motion = el.motionInstance
69
+ motion.clickIds = clicks.map(i => i.id)
70
+ motion.set(variantInitial)
71
+ motion.watchStopHandle = watch(
72
+ [thisPage, currentPage, currentClicks].filter(Boolean),
73
+ () => {
74
+ const visibility = clickVisibility?.value ?? true
75
+ if (!clicksContext?.value || !['slide', 'presenter'].includes(renderContext?.value ?? '')) {
76
+ const mixedVariant: Record<string, unknown> = { ...variantInitial, ...variantEnter }
77
+ for (const { variant } of clicks)
78
+ Object.assign(mixedVariant, variant)
79
+
80
+ motion.set(mixedVariant)
81
+ }
82
+ else if (isPrintMode.value || thisPage?.value === currentPage.value) {
83
+ if (visibility === true) {
84
+ const mixedVariant: Record<string, unknown> = { ...variantInitial, ...variantEnter }
85
+ for (const { variant, resolved: resolvedClick } of clicks) {
86
+ if (!resolvedClick || resolvedClick.isActive.value)
87
+ Object.assign(mixedVariant, variant)
88
+ }
89
+ if (isPrintMode.value)
90
+ motion.set(mixedVariant) // print with clicks
91
+ else
92
+ motion.apply(mixedVariant)
93
+ }
94
+ else {
95
+ motion.apply(visibility === 'before' ? variantInitial : variantLeave)
96
+ }
97
+ }
98
+ else {
99
+ motion.apply((thisPage?.value ?? -1) > currentPage.value ? variantInitial : variantLeave)
100
+ }
101
+ },
102
+ {
103
+ immediate: true,
104
+ },
105
+ )
106
+ },
107
+ unmounted(el, dir) {
108
+ if (!directiveInject(dir, injectionClicksContext)?.value)
109
+ return
110
+
111
+ const ctx = directiveInject(dir, injectionClicksContext)?.value
112
+ // @ts-expect-error extra prop
113
+ const motion = el.motionInstance
114
+ motion.clickIds.map((id: string) => ctx?.unregister(id))
115
+ motion.watchStopHandle()
116
+ },
117
+ })
118
+ },
119
+ }
120
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@slidev/client",
3
3
  "type": "module",
4
- "version": "0.48.7",
4
+ "version": "0.48.9",
5
5
  "description": "Presentation slides for developers",
6
6
  "author": "antfu <anthonyfu117@hotmail.com>",
7
7
  "license": "MIT",
@@ -32,12 +32,12 @@
32
32
  "@iconify-json/carbon": "^1.1.31",
33
33
  "@iconify-json/ph": "^1.1.11",
34
34
  "@iconify-json/svg-spinners": "^1.1.2",
35
- "@shikijs/monaco": "^1.1.7",
36
- "@shikijs/vitepress-twoslash": "^1.1.7",
35
+ "@shikijs/monaco": "^1.2.3",
36
+ "@shikijs/vitepress-twoslash": "^1.2.3",
37
37
  "@slidev/rough-notation": "^0.1.0",
38
38
  "@typescript/ata": "^0.9.4",
39
- "@unhead/vue": "^1.8.18",
40
- "@unocss/reset": "^0.58.5",
39
+ "@unhead/vue": "^1.9.4",
40
+ "@unocss/reset": "^0.58.9",
41
41
  "@vueuse/core": "^10.9.0",
42
42
  "@vueuse/math": "^10.9.0",
43
43
  "@vueuse/motion": "^2.1.0",
@@ -47,23 +47,23 @@
47
47
  "floating-vue": "^5.2.2",
48
48
  "fuse.js": "^7.0.0",
49
49
  "js-yaml": "^4.1.0",
50
- "katex": "^0.16.9",
50
+ "katex": "^0.16.10",
51
51
  "lz-string": "^1.5.0",
52
52
  "mermaid": "^10.9.0",
53
53
  "monaco-editor": "^0.47.0",
54
54
  "prettier": "^3.2.5",
55
55
  "recordrtc": "^5.6.2",
56
- "shiki": "^1.1.7",
57
- "shiki-magic-move": "^0.3.4",
58
- "typescript": "^5.4.2",
59
- "unocss": "^0.58.5",
56
+ "shiki": "^1.2.3",
57
+ "shiki-magic-move": "^0.3.5",
58
+ "typescript": "^5.4.3",
59
+ "unocss": "^0.58.9",
60
60
  "vue": "^3.4.21",
61
61
  "vue-demi": "^0.14.7",
62
62
  "vue-router": "^4.3.0",
63
- "@slidev/types": "0.48.7",
64
- "@slidev/parser": "0.48.7"
63
+ "@slidev/parser": "0.48.9",
64
+ "@slidev/types": "0.48.9"
65
65
  },
66
66
  "devDependencies": {
67
- "vite": "^5.1.6"
67
+ "vite": "^5.2.7"
68
68
  }
69
69
  }
package/pages/print.vue CHANGED
@@ -28,8 +28,8 @@ onMounted(() => {
28
28
  :style="{ background: 'var(--slidev-slide-container-background, black)' }"
29
29
  :width="windowSize.width.value"
30
30
  />
31
+ <div id="twoslash-container" />
31
32
  </div>
32
- <div id="twoslash-container" />
33
33
  </template>
34
34
 
35
35
  <style lang="postcss">
@@ -1,13 +1,14 @@
1
- import { createSingletonPromise } from '@antfu/utils'
1
+ import { createSingletonPromise, ensurePrefix, slash } from '@antfu/utils'
2
2
  import type { CodeRunner, CodeRunnerContext, CodeRunnerOutput, CodeRunnerOutputText, CodeRunnerOutputs } from '@slidev/types'
3
3
  import type { CodeToHastOptions } from 'shiki'
4
+ import type ts from 'typescript'
4
5
  import { isDark } from '../logic/dark'
5
6
  import setups from '#slidev/setups/code-runners'
6
7
 
7
8
  export default createSingletonPromise(async () => {
8
9
  const runners: Record<string, CodeRunner> = {
9
- javascript: runJavaScript,
10
- js: runJavaScript,
10
+ javascript: runTypeScript,
11
+ js: runTypeScript,
11
12
  typescript: runTypeScript,
12
13
  ts: runTypeScript,
13
14
  }
@@ -24,6 +25,18 @@ export default createSingletonPromise(async () => {
24
25
  ...options,
25
26
  })
26
27
 
28
+ const resolveId = async (specifier: string) => {
29
+ if (!/^(@[^\/:]+?\/)?[^\/:]+$/.test(specifier))
30
+ return specifier
31
+ const res = await fetch(`/@slidev/resolve-id/${specifier}`)
32
+ if (!res.ok)
33
+ return null
34
+ const id = await res.text()
35
+ if (!id)
36
+ return null
37
+ return `/@fs${ensurePrefix('/', slash(id))}`
38
+ }
39
+
27
40
  const run = async (code: string, lang: string, options: Record<string, unknown>): Promise<CodeRunnerOutputs> => {
28
41
  try {
29
42
  const runner = runners[lang]
@@ -34,6 +47,7 @@ export default createSingletonPromise(async () => {
34
47
  {
35
48
  options,
36
49
  highlight,
50
+ resolveId,
37
51
  run: async (code, lang) => {
38
52
  return await run(code, lang, options)
39
53
  },
@@ -60,7 +74,7 @@ export default createSingletonPromise(async () => {
60
74
  })
61
75
 
62
76
  // Ported from https://github.com/microsoft/TypeScript-Website/blob/v2/packages/playground/src/sidebar/runtime.ts
63
- export async function runJavaScript(code: string): Promise<CodeRunnerOutputs> {
77
+ async function runJavaScript(code: string): Promise<CodeRunnerOutputs> {
64
78
  const allLogs: CodeRunnerOutput[] = []
65
79
 
66
80
  const replace = {} as any
@@ -144,9 +158,13 @@ export async function runJavaScript(code: string): Promise<CodeRunnerOutputs> {
144
158
  return textRep
145
159
  }
146
160
 
147
- // The reflect-metadata runtime is available, so allow that to go through
148
161
  function sanitizeJS(code: string) {
149
- return code.replace(`import "reflect-metadata"`, '').replace(`require("reflect-metadata")`, '')
162
+ // The reflect-metadata runtime is available, so allow that to go through
163
+ code = code.replace(`import "reflect-metadata"`, '').replace(`require("reflect-metadata")`, '')
164
+ // Transpiled typescript sometimes contains an empty export, remove it.
165
+ code = code.replace('export {};', '')
166
+
167
+ return code
150
168
  }
151
169
 
152
170
  return allLogs
@@ -155,10 +173,80 @@ export async function runJavaScript(code: string): Promise<CodeRunnerOutputs> {
155
173
  let tsModule: typeof import('typescript') | undefined
156
174
 
157
175
  export async function runTypeScript(code: string, context: CodeRunnerContext) {
158
- const { transpile } = tsModule ??= await import('typescript')
159
- code = transpile(code, {
160
- module: tsModule.ModuleKind.ESNext,
161
- target: tsModule.ScriptTarget.ES2022,
162
- })
163
- return await context.run(code, 'javascript')
176
+ tsModule ??= await import('typescript')
177
+
178
+ code = tsModule.transpileModule(code, {
179
+ compilerOptions: {
180
+ module: tsModule.ModuleKind.ESNext,
181
+ target: tsModule.ScriptTarget.ES2022,
182
+ },
183
+ transformers: {
184
+ after: [transformImports],
185
+ },
186
+ }).outputText
187
+
188
+ const importRegex = /import\s*\(\s*(['"])(.+?)['"]\s*\)/g
189
+ const idMap: Record<string, string> = {}
190
+ for (const [,,specifier] of code.matchAll(importRegex)!)
191
+ idMap[specifier] = await context.resolveId(specifier) ?? specifier
192
+ code = code.replace(importRegex, (_full, quote, specifier) => `import(${quote}${idMap[specifier] ?? specifier}${quote})`)
193
+
194
+ return await runJavaScript(code)
195
+ }
196
+
197
+ /**
198
+ * Transform import statements to dynamic imports
199
+ */
200
+ function transformImports(context: ts.TransformationContext): ts.Transformer<ts.SourceFile> {
201
+ const { factory } = context
202
+ const { isImportDeclaration, isNamedImports, NodeFlags } = tsModule!
203
+ return (sourceFile: ts.SourceFile) => {
204
+ const statements = [...sourceFile.statements]
205
+ for (let i = 0; i < statements.length; i++) {
206
+ const statement = statements[i]
207
+ if (!isImportDeclaration(statement))
208
+ continue
209
+ let bindingPattern: ts.ObjectBindingPattern | ts.Identifier
210
+ const namedBindings = statement.importClause?.namedBindings
211
+ const bindings: ts.BindingElement[] = []
212
+ if (statement.importClause?.name)
213
+ bindings.push(factory.createBindingElement(undefined, factory.createIdentifier('default'), statement.importClause.name))
214
+ if (namedBindings) {
215
+ if (isNamedImports(namedBindings)) {
216
+ for (const specifier of namedBindings.elements)
217
+ bindings.push(factory.createBindingElement(undefined, specifier.propertyName, specifier.name))
218
+ bindingPattern = factory.createObjectBindingPattern(bindings)
219
+ }
220
+ else {
221
+ bindingPattern = factory.createIdentifier(namedBindings.name.text)
222
+ }
223
+ }
224
+ else {
225
+ bindingPattern = factory.createObjectBindingPattern(bindings)
226
+ }
227
+
228
+ const newStatement = factory.createVariableStatement(
229
+ undefined,
230
+ factory.createVariableDeclarationList(
231
+ [
232
+ factory.createVariableDeclaration(
233
+ bindingPattern,
234
+ undefined,
235
+ undefined,
236
+ factory.createAwaitExpression(
237
+ factory.createCallExpression(
238
+ factory.createIdentifier('import'),
239
+ undefined,
240
+ [statement.moduleSpecifier],
241
+ ),
242
+ ),
243
+ ),
244
+ ],
245
+ NodeFlags.Const,
246
+ ),
247
+ )
248
+ statements[i] = newStatement
249
+ }
250
+ return factory.updateSourceFile(sourceFile, statements)
251
+ }
164
252
  }
package/setup/main.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import type { AppContext } from '@slidev/types'
2
- import { MotionPlugin } from '@vueuse/motion'
3
2
  import TwoSlashFloatingVue from '@shikijs/vitepress-twoslash/client'
4
3
  import type { App } from 'vue'
5
4
  import { nextTick } from 'vue'
@@ -8,6 +7,7 @@ import { createHead } from '@unhead/vue'
8
7
  import { routeForceRefresh } from '../logic/route'
9
8
  import { createVClickDirectives } from '../modules/v-click'
10
9
  import { createVMarkDirective } from '../modules/v-mark'
10
+ import { createVMotionDirectives } from '../modules/v-motion'
11
11
  import { routes } from '../routes'
12
12
  import setups from '#slidev/setups/main'
13
13
 
@@ -34,7 +34,7 @@ export default async function setupMain(app: App) {
34
34
  app.use(createHead())
35
35
  app.use(createVClickDirectives())
36
36
  app.use(createVMarkDirective())
37
- app.use(MotionPlugin)
37
+ app.use(createVMotionDirectives())
38
38
  app.use(TwoSlashFloatingVue as any, { container: '#twoslash-container' })
39
39
 
40
40
  const context: AppContext = {
package/utils.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { SlideRoute } from '@slidev/types'
2
+ import type { DirectiveBinding, InjectionKey } from 'vue'
2
3
  import { configs } from './env'
3
4
 
4
5
  export function getSlideClass(route?: SlideRoute, extra = '') {
@@ -22,3 +23,18 @@ export async function downloadPDF() {
22
23
  `${configs.title}.pdf`,
23
24
  )
24
25
  }
26
+
27
+ export function directiveInject<T = unknown>(dir: DirectiveBinding<any>, key: InjectionKey<T> | string, defaultValue?: T): T | undefined {
28
+ return (dir.instance?.$ as any).provides[key as any] ?? defaultValue
29
+ }
30
+
31
+ export function directiveProvide<T = unknown>(dir: DirectiveBinding<any>, key: InjectionKey<T> | string, value?: T) {
32
+ const instance = dir.instance?.$ as any
33
+ if (instance) {
34
+ let provides = instance.provides
35
+ const parentProvides = instance.parent?.provides
36
+ if (provides === parentProvides)
37
+ provides = instance.provides = Object.create(parentProvides)
38
+ provides[key as any] = value
39
+ }
40
+ }