@slidev/client 0.48.0-beta.20 → 0.48.0-beta.22

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.
Files changed (49) hide show
  1. package/builtin/CodeBlockWrapper.vue +23 -25
  2. package/builtin/ShikiMagicMove.vue +63 -13
  3. package/builtin/SlidevVideo.vue +1 -1
  4. package/builtin/Toc.vue +1 -1
  5. package/builtin/TocList.vue +1 -1
  6. package/composables/useClicks.ts +16 -15
  7. package/composables/useContext.ts +4 -9
  8. package/composables/useNav.ts +182 -44
  9. package/composables/useSwipeControls.ts +40 -0
  10. package/composables/useTocTree.ts +63 -0
  11. package/constants.ts +2 -3
  12. package/context.ts +3 -6
  13. package/env.ts +4 -4
  14. package/internals/Goto.vue +2 -2
  15. package/internals/NavControls.vue +7 -5
  16. package/internals/NoteStatic.vue +1 -1
  17. package/internals/PrintContainer.vue +8 -7
  18. package/internals/PrintSlide.vue +6 -13
  19. package/internals/PrintSlideClick.vue +11 -13
  20. package/internals/QuickOverview.vue +10 -10
  21. package/internals/SideEditor.vue +3 -3
  22. package/internals/SlideContainer.vue +6 -6
  23. package/internals/SlideLoading.vue +19 -0
  24. package/internals/SlideWrapper.ts +12 -12
  25. package/internals/SlidesShow.vue +13 -10
  26. package/layouts/error.vue +5 -0
  27. package/logic/drawings.ts +7 -7
  28. package/logic/nav-state.ts +20 -0
  29. package/logic/nav.ts +49 -259
  30. package/logic/note.ts +2 -2
  31. package/logic/overview.ts +2 -2
  32. package/logic/route.ts +10 -1
  33. package/logic/slides.ts +19 -0
  34. package/logic/transition.ts +50 -0
  35. package/logic/utils.ts +24 -0
  36. package/modules/context.ts +7 -12
  37. package/package.json +8 -7
  38. package/pages/notes.vue +2 -3
  39. package/pages/overview.vue +19 -21
  40. package/pages/play.vue +2 -1
  41. package/pages/presenter/print.vue +2 -2
  42. package/pages/presenter.vue +15 -14
  43. package/routes.ts +6 -14
  44. package/setup/root.ts +6 -7
  45. package/setup/shortcuts.ts +2 -1
  46. package/shim-vue.d.ts +3 -0
  47. package/state/index.ts +1 -1
  48. package/styles/code.css +2 -2
  49. package/utils.ts +15 -2
@@ -12,13 +12,12 @@ Learn more: https://sli.dev/guide/syntax.html#line-highlighting
12
12
  -->
13
13
 
14
14
  <script setup lang="ts">
15
- import { parseRangeString } from '@slidev/parser/core'
16
15
  import { useClipboard } from '@vueuse/core'
17
16
  import { computed, onMounted, onUnmounted, ref, watchEffect } from 'vue'
18
17
  import type { PropType } from 'vue'
19
18
  import { configs } from '../env'
20
- import { makeId } from '../logic/utils'
21
- import { CLASS_VCLICK_HIDDEN, CLASS_VCLICK_TARGET } from '../constants'
19
+ import { makeId, updateCodeHighlightRange } from '../logic/utils'
20
+ import { CLASS_VCLICK_HIDDEN } from '../constants'
22
21
  import { useSlideContext } from '../context'
23
22
 
24
23
  const props = defineProps({
@@ -87,28 +86,27 @@ onMounted(() => {
87
86
  if (hide)
88
87
  rangeStr = props.ranges[index.value + 1] ?? finallyRange.value
89
88
 
90
- const isDuoTone = el.value.querySelector('.shiki-dark')
91
- const targets = isDuoTone ? Array.from(el.value.querySelectorAll('.shiki')) : [el.value]
92
- const startLine = props.startLine
93
- for (const target of targets) {
94
- const lines = Array.from(target.querySelectorAll('code > .line'))
95
- const highlights: number[] = parseRangeString(lines.length + startLine - 1, rangeStr)
96
- lines.forEach((line, idx) => {
97
- const highlighted = highlights.includes(idx + startLine)
98
- line.classList.toggle(CLASS_VCLICK_TARGET, true)
99
- line.classList.toggle('highlighted', highlighted)
100
- line.classList.toggle('dishonored', !highlighted)
101
- })
102
- if (props.maxHeight) {
103
- const highlightedEls = Array.from(target.querySelectorAll('.line.highlighted')) as HTMLElement[]
104
- const height = highlightedEls.reduce((acc, el) => el.offsetHeight + acc, 0)
105
- if (height > el.value.offsetHeight) {
106
- highlightedEls[0].scrollIntoView({ behavior: 'smooth', block: 'start' })
107
- }
108
- else if (highlightedEls.length > 0) {
109
- const middleEl = highlightedEls[Math.round((highlightedEls.length - 1) / 2)]
110
- middleEl.scrollIntoView({ behavior: 'smooth', block: 'center' })
111
- }
89
+ const pre = el.value.querySelector('.shiki')!
90
+ const lines = Array.from(pre.querySelectorAll('code > .line'))
91
+ const linesCount = lines.length
92
+
93
+ updateCodeHighlightRange(
94
+ rangeStr,
95
+ linesCount,
96
+ props.startLine,
97
+ no => [lines[no]],
98
+ )
99
+
100
+ // Scroll to the highlighted line if `maxHeight` is set
101
+ if (props.maxHeight) {
102
+ const highlightedEls = Array.from(pre.querySelectorAll('.line.highlighted')) as HTMLElement[]
103
+ const height = highlightedEls.reduce((acc, el) => el.offsetHeight + acc, 0)
104
+ if (height > el.value.offsetHeight) {
105
+ highlightedEls[0].scrollIntoView({ behavior: 'smooth', block: 'start' })
106
+ }
107
+ else if (highlightedEls.length > 0) {
108
+ const middleEl = highlightedEls[Math.round((highlightedEls.length - 1) / 2)]
109
+ middleEl.scrollIntoView({ behavior: 'smooth', block: 'center' })
112
110
  }
113
111
  }
114
112
  })
@@ -1,22 +1,28 @@
1
1
  <script setup lang="ts">
2
2
  import { ShikiMagicMovePrecompiled } from 'shiki-magic-move/vue'
3
3
  import type { KeyedTokensInfo } from 'shiki-magic-move/types'
4
- import { onMounted, onUnmounted, ref, watchEffect } from 'vue'
4
+ import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
5
5
  import lz from 'lz-string'
6
6
  import { useSlideContext } from '../context'
7
- import { makeId } from '../logic/utils'
7
+ import { makeId, updateCodeHighlightRange } from '../logic/utils'
8
8
 
9
9
  import 'shiki-magic-move/style.css'
10
10
 
11
11
  const props = defineProps<{
12
- stepsLz: string
13
12
  at?: string | number
13
+ stepsLz: string
14
+ stepRanges: string[][]
14
15
  }>()
15
16
 
16
17
  const steps = JSON.parse(lz.decompressFromBase64(props.stepsLz)) as KeyedTokensInfo[]
17
18
  const { $clicksContext: clicks, $scale: scale } = useSlideContext()
18
19
  const id = makeId()
19
- const index = ref(0)
20
+
21
+ const stepIndex = ref(0)
22
+ const container = ref<HTMLElement>()
23
+
24
+ // Normalized the ranges, to at least have one range
25
+ const ranges = computed(() => props.stepRanges.map(i => i.length ? i : ['all']))
20
26
 
21
27
  onUnmounted(() => {
22
28
  clicks!.unregister(id)
@@ -26,24 +32,68 @@ onMounted(() => {
26
32
  if (!clicks || clicks.disabled)
27
33
  return
28
34
 
29
- const { start, end, delta } = clicks.resolve(props.at || '+1', steps.length - 1)
35
+ if (ranges.value.length !== steps.length)
36
+ throw new Error('[slidev] The length of stepRanges does not match the length of steps, this is an internal error.')
37
+
38
+ const clickCounts = ranges.value.map(s => s.length).reduce((a, b) => a + b, 0)
39
+ const { start, end, delta } = clicks.resolve(props.at ?? '+1', clickCounts - 1)
30
40
  clicks.register(id, { max: end, delta })
31
41
 
32
- watchEffect(() => {
33
- if (clicks.disabled)
34
- index.value = steps.length - 1
35
- else
36
- index.value = Math.min(Math.max(0, clicks.current - start + 1), steps.length - 1)
37
- })
42
+ watch(
43
+ () => clicks.current,
44
+ () => {
45
+ // Calculate the step and rangeStr based on the current click count
46
+ const clickCount = clicks.current - start
47
+ let step = steps.length - 1
48
+ let _currentClickSum = 0
49
+ let rangeStr = 'all'
50
+ for (let i = 0; i < ranges.value.length; i++) {
51
+ const current = ranges.value[i]
52
+ if (clickCount < _currentClickSum + current.length - 1) {
53
+ step = i
54
+ rangeStr = current[clickCount - _currentClickSum + 1]
55
+ break
56
+ }
57
+ _currentClickSum += current.length || 1
58
+ }
59
+ stepIndex.value = step
60
+
61
+ const pre = container.value?.querySelector('.shiki') as HTMLElement
62
+ if (!pre)
63
+ return
64
+
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
68
+
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[][])
77
+
78
+ // Update highlight range
79
+ updateCodeHighlightRange(
80
+ rangeStr,
81
+ lines.length,
82
+ 1,
83
+ no => lines[no],
84
+ )
85
+ },
86
+ { immediate: true },
87
+ )
38
88
  })
39
89
  </script>
40
90
 
41
91
  <template>
42
- <div class="slidev-code-wrapper slidev-code-magic-move">
92
+ <div ref="container" class="slidev-code-wrapper slidev-code-magic-move relative">
43
93
  <ShikiMagicMovePrecompiled
44
94
  class="slidev-code relative shiki overflow-visible"
45
95
  :steps="steps"
46
- :step="index"
96
+ :step="stepIndex"
47
97
  :options="{ globalScale: scale }"
48
98
  />
49
99
  </div>
@@ -22,7 +22,7 @@ const ended = ref(false)
22
22
  const matchRoute = computed(() => {
23
23
  if (!video.value || currentContext?.value !== 'slide')
24
24
  return false
25
- return route === $slidev?.nav.currentRoute
25
+ return route && route.no === $slidev?.nav.currentSlideNo
26
26
  })
27
27
 
28
28
  const matchClick = computed(() => {
package/builtin/Toc.vue CHANGED
@@ -74,7 +74,7 @@ function filterOnlySiblings(tree: TocItem[]): TocItem[] {
74
74
  }
75
75
 
76
76
  const toc = computed(() => {
77
- const tree = $slidev?.nav.tree
77
+ const tree = $slidev?.nav.tocTree
78
78
  if (!tree)
79
79
  return []
80
80
  let tocTree = filterTreeDepth(tree)
@@ -48,7 +48,7 @@ const styles = computed(() => {
48
48
  :class="[{ 'slidev-toc-item-active': item.active }, { 'slidev-toc-item-parent-active': item.activeParent }]"
49
49
  >
50
50
  <Link :to="item.path">
51
- <Titles :no="item.path" />
51
+ <Titles :no="item.no" />
52
52
  </Link>
53
53
  <TocList
54
54
  v-if="item.children.length > 0"
@@ -1,11 +1,11 @@
1
1
  import { sum } from '@antfu/utils'
2
- import type { ClicksContext } from '@slidev/types'
2
+ import type { ClicksContext, SlideRoute } from '@slidev/types'
3
3
  import type { Ref } from 'vue'
4
4
  import { computed, ref, shallowReactive } from 'vue'
5
- import type { RouteRecordRaw } from 'vue-router'
6
- import { currentRoute, isPrintMode, isPrintWithClicks, queryClicks, routeForceRefresh } from '../logic/nav'
5
+ import { currentSlideNo, isPrintMode, isPrintWithClicks } from '../logic/nav'
7
6
  import { normalizeAtProp } from '../logic/utils'
8
7
  import { CLICKS_MAX } from '../constants'
8
+ import { routeForceRefresh, useRouteQuery } from '../logic/route'
9
9
 
10
10
  function useClicksContextBase(current: Ref<number>, clicksOverrides?: number): ClicksContext {
11
11
  const relativeOffsets: ClicksContext['relativeOffsets'] = new Map()
@@ -63,27 +63,28 @@ function useClicksContextBase(current: Ref<number>, clicksOverrides?: number): C
63
63
  }
64
64
  }
65
65
 
66
- export function usePrimaryClicks(route: RouteRecordRaw | undefined): ClicksContext {
66
+ const queryClicksRaw = useRouteQuery('clicks', '0')
67
+
68
+ export function usePrimaryClicks(route: SlideRoute): ClicksContext {
67
69
  if (route?.meta?.__clicksContext)
68
70
  return route.meta.__clicksContext
69
- const thisPath = +(route?.path ?? Number.NaN)
71
+ const thisNo = route.no
70
72
  const current = computed({
71
73
  get() {
72
- const currentPath = +(currentRoute.value?.path ?? Number.NaN)
73
- if (!currentPath || Number.isNaN(currentPath))
74
- return 0
75
- if (currentPath === thisPath)
76
- return queryClicks.value
77
- else if (currentPath > thisPath)
74
+ // eslint-disable-next-line ts/no-use-before-define
75
+ if (context.disabled)
76
+ return CLICKS_MAX
77
+ if (currentSlideNo.value === thisNo)
78
+ return +(queryClicksRaw.value || 0) || 0
79
+ else if (currentSlideNo.value > thisNo)
78
80
  return CLICKS_MAX
79
81
  else
80
82
  return 0
81
83
  },
82
84
  set(v) {
83
- const currentPath = +(currentRoute.value?.path ?? Number.NaN)
84
- if (currentPath === thisPath) {
85
+ if (currentSlideNo.value === thisNo) {
85
86
  // eslint-disable-next-line ts/no-use-before-define
86
- queryClicks.value = Math.min(v, context.total)
87
+ queryClicksRaw.value = Math.min(v, context.total).toString()
87
88
  }
88
89
  },
89
90
  })
@@ -96,6 +97,6 @@ export function usePrimaryClicks(route: RouteRecordRaw | undefined): ClicksConte
96
97
  return context
97
98
  }
98
99
 
99
- export function useFixedClicks(route?: RouteRecordRaw | undefined, currentInit = 0): ClicksContext {
100
+ export function useFixedClicks(route?: SlideRoute | undefined, currentInit = 0): ClicksContext {
100
101
  return useClicksContextBase(ref(currentInit), route?.meta?.clicks)
101
102
  }
@@ -1,16 +1,11 @@
1
- import type { ComputedRef } from 'vue'
2
1
  import { computed } from 'vue'
3
- import type { RouteLocationNormalizedLoaded } from 'vue-router'
4
- import type { SlidevContext } from '../modules/context'
5
2
  import { configs } from '../env'
6
- import { useNav } from './useNav'
3
+ import type { SlidevContext } from '../modules/context'
4
+ import * as nav from '../logic/nav'
7
5
 
8
- export function useContext(
9
- route: ComputedRef<RouteLocationNormalizedLoaded>,
10
- ): SlidevContext {
11
- const nav = useNav(route)
6
+ export function useContext(): SlidevContext {
12
7
  return {
13
- nav,
8
+ nav: { ...nav }, // Convert the module to a plain object
14
9
  configs,
15
10
  themeConfigs: computed(() => configs.themeConfig),
16
11
  }
@@ -1,55 +1,193 @@
1
- import type { ComputedRef } from 'vue'
2
- import { computed } from 'vue'
3
- import type { RouteLocationNormalizedLoaded, RouteRecordRaw } from 'vue-router'
4
- import type { TocItem } from '@slidev/types'
5
- import type { SlidevContextNav } from '../modules/context'
6
- import { addToTree, clicks, clicksContext, clicksTotal, downloadPDF, filterTree, getPath, getTreeWithActiveStatuses, go, next, nextSlide, openInEditor, prev, prevSlide } from '../logic/nav'
7
- import { rawRoutes } from '../routes'
8
-
9
- export function useNav(route: ComputedRef<RouteRecordRaw | RouteLocationNormalizedLoaded>): SlidevContextNav {
10
- const path = computed(() => route.value.path)
11
- const total = computed(() => rawRoutes.length)
12
-
13
- const currentPage = computed(() => Number.parseInt(path.value.split(/\//g).slice(-1)[0]) || 1)
14
- const currentPath = computed(() => getPath(currentPage.value))
15
- const currentRoute = computed(() => rawRoutes.find(i => i.path === `${currentPage.value}`) ?? rawRoutes.at(-1) ?? rawRoutes[0])
16
- const currentSlideId = computed(() => currentRoute.value?.meta?.slide?.id)
17
- const currentLayout = computed(() => currentRoute.value?.meta?.layout || (currentPage.value === 1 ? 'cover' : 'default'))
18
-
19
- const nextRoute = computed(() => rawRoutes.find(i => i.path === `${Math.min(rawRoutes.length, currentPage.value + 1)}`))
20
-
21
- const rawTree = computed(() => rawRoutes
22
- .filter((route: RouteRecordRaw) => route.meta?.slide?.title)
23
- .reduce((acc: TocItem[], route: RouteRecordRaw) => {
24
- addToTree(acc, route)
25
- return acc
26
- }, []))
27
- const treeWithActiveStatuses = computed(() => getTreeWithActiveStatuses(rawTree.value, currentRoute.value))
28
- const tree = computed(() => filterTree(treeWithActiveStatuses.value))
1
+ import type { ClicksContext, SlideRoute, TocItem } from '@slidev/types'
2
+ import type { ComputedRef, Ref, TransitionGroupProps } from 'vue'
3
+ import { computed, ref, watch } from 'vue'
4
+ import type { Router } from 'vue-router'
5
+ import { getCurrentTransition } from '../logic/transition'
6
+ import { getSlidePath } from '../logic/slides'
7
+ import { useTocTree } from './useTocTree'
8
+ import { skipTransition } from './hmr'
9
+ import { slides } from '#slidev/slides'
10
+
11
+ export interface SlidevContextNav {
12
+ slides: Ref<SlideRoute[]>
13
+ total: ComputedRef<number>
14
+
15
+ currentPath: ComputedRef<string>
16
+ currentPage: ComputedRef<number>
17
+ currentSlideNo: ComputedRef<number>
18
+ currentSlideRoute: ComputedRef<SlideRoute>
19
+ currentTransition: ComputedRef<TransitionGroupProps | undefined>
20
+ currentLayout: ComputedRef<string>
21
+
22
+ nextRoute: ComputedRef<SlideRoute>
23
+ prevRoute: ComputedRef<SlideRoute>
24
+ hasNext: ComputedRef<boolean>
25
+ hasPrev: ComputedRef<boolean>
26
+
27
+ clicksContext: ComputedRef<ClicksContext>
28
+ clicks: ComputedRef<number>
29
+ clicksTotal: ComputedRef<number>
30
+
31
+ /** The table of content tree */
32
+ tocTree: ComputedRef<TocItem[]>
33
+ /** The direction of the navigation, 1 for forward, -1 for backward */
34
+ navDirection: Ref<number>
35
+ /** The direction of the clicks, 1 for forward, -1 for backward */
36
+ clicksDirection: Ref<number>
37
+ /** Utility function for open file in editor, only avaible in dev mode */
38
+ openInEditor: (url?: string) => Promise<boolean>
39
+
40
+ /** Go to next click */
41
+ next: () => Promise<void>
42
+ /** Go to previous click */
43
+ prev: () => Promise<void>
44
+ /** Go to next slide */
45
+ nextSlide: () => Promise<void>
46
+ /** Go to previous slide */
47
+ prevSlide: (lastClicks?: boolean) => Promise<void>
48
+ /** Go to slide */
49
+ go: (page: number | string, clicks?: number) => Promise<void>
50
+ /** Go to the first slide */
51
+ goFirst: () => Promise<void>
52
+ /** Go to the last slide */
53
+ goLast: () => Promise<void>
54
+ }
55
+
56
+ export function useNavBase(
57
+ currentSlideRoute: ComputedRef<SlideRoute>,
58
+ clicksContext: ComputedRef<ClicksContext>,
59
+ queryClicks: Ref<number> = ref(0),
60
+ router?: Router,
61
+ ): SlidevContextNav {
62
+ const total = computed(() => slides.value.length)
63
+
64
+ const navDirection = ref(0)
65
+ const clicksDirection = ref(0)
66
+
67
+ const currentPath = computed(() => getSlidePath(currentSlideRoute.value))
68
+ const currentSlideNo = computed(() => currentSlideRoute.value.no)
69
+ const currentLayout = computed(() => currentSlideRoute.value.meta?.layout || (currentSlideNo.value === 1 ? 'cover' : 'default'))
70
+
71
+ const clicks = computed(() => clicksContext.value.current)
72
+ const clicksTotal = computed(() => clicksContext.value.total)
73
+ const nextRoute = computed(() => slides.value[Math.min(slides.value.length, currentSlideNo.value + 1) - 1])
74
+ const prevRoute = computed(() => slides.value[Math.max(1, currentSlideNo.value - 1) - 1])
75
+ const hasNext = computed(() => currentSlideNo.value < slides.value.length || clicks.value < clicksTotal.value)
76
+ const hasPrev = computed(() => currentSlideNo.value > 1 || clicks.value > 0)
77
+
78
+ const currentTransition = computed(() => getCurrentTransition(navDirection.value, currentSlideRoute.value, prevRoute.value))
79
+
80
+ watch(currentSlideRoute, (next, prev) => {
81
+ navDirection.value = next.no - prev.no
82
+ })
83
+
84
+ async function openInEditor(url?: string) {
85
+ if (!__DEV__)
86
+ return false
87
+ if (url == null) {
88
+ const slide = currentSlideRoute.value?.meta?.slide
89
+ if (!slide)
90
+ return false
91
+ url = `${slide.filepath}:${slide.start}`
92
+ }
93
+ await fetch(`/__open-in-editor?file=${encodeURIComponent(url)}`)
94
+ return true
95
+ }
96
+
97
+ const tocTree = useTocTree(slides)
98
+
99
+ async function next() {
100
+ clicksDirection.value = 1
101
+ if (clicksTotal.value <= queryClicks.value)
102
+ await nextSlide()
103
+ else
104
+ queryClicks.value += 1
105
+ }
106
+
107
+ async function prev() {
108
+ clicksDirection.value = -1
109
+ if (queryClicks.value <= 0)
110
+ await prevSlide()
111
+ else
112
+ queryClicks.value -= 1
113
+ }
114
+
115
+ async function nextSlide() {
116
+ clicksDirection.value = 1
117
+ if (currentSlideNo.value < slides.value.length)
118
+ await go(currentSlideNo.value + 1)
119
+ }
120
+
121
+ async function prevSlide(lastClicks = true) {
122
+ clicksDirection.value = -1
123
+ const next = Math.max(1, currentSlideNo.value - 1)
124
+ await go(next)
125
+ if (lastClicks && clicksTotal.value) {
126
+ router?.replace({
127
+ query: { ...router.currentRoute.value.query, clicks: clicksTotal.value },
128
+ })
129
+ }
130
+ }
131
+
132
+ function goFirst() {
133
+ return go(1)
134
+ }
135
+
136
+ function goLast() {
137
+ return go(total.value)
138
+ }
139
+
140
+ async function go(page: number | string, clicks?: number) {
141
+ skipTransition.value = false
142
+ await router?.push({
143
+ path: getSlidePath(page),
144
+ query: { ...router.currentRoute.value.query, clicks },
145
+ })
146
+ }
29
147
 
30
148
  return {
31
- rawRoutes,
32
- route,
33
- path,
149
+ slides,
34
150
  total,
35
- clicksContext,
36
- clicks,
37
- clicksTotal,
38
- currentPage,
39
151
  currentPath,
40
- currentRoute,
41
- currentSlideId,
152
+ currentSlideNo,
153
+ currentPage: currentSlideNo,
154
+ currentSlideRoute,
42
155
  currentLayout,
156
+ currentTransition,
157
+ clicksDirection,
43
158
  nextRoute,
44
- rawTree,
45
- treeWithActiveStatuses,
46
- tree,
47
- go,
48
- downloadPDF,
49
- next,
50
- nextSlide,
159
+ prevRoute,
160
+ clicksContext,
161
+ clicks,
162
+ clicksTotal,
163
+ hasNext,
164
+ hasPrev,
165
+ tocTree,
166
+ navDirection,
51
167
  openInEditor,
168
+ next,
52
169
  prev,
170
+ go,
171
+ goLast,
172
+ goFirst,
173
+ nextSlide,
53
174
  prevSlide,
54
175
  }
55
176
  }
177
+
178
+ export function useFixedNav(
179
+ currentSlideRoute: SlideRoute,
180
+ clicksContext: ClicksContext,
181
+ ): SlidevContextNav {
182
+ const noop = async () => { }
183
+ return {
184
+ ...useNavBase(computed(() => currentSlideRoute), computed(() => clicksContext)),
185
+ next: noop,
186
+ prev: noop,
187
+ nextSlide: noop,
188
+ prevSlide: noop,
189
+ goFirst: noop,
190
+ goLast: noop,
191
+ go: noop,
192
+ }
193
+ }
@@ -0,0 +1,40 @@
1
+ import type { Ref } from 'vue'
2
+ import { ref } from 'vue'
3
+ import { timestamp, usePointerSwipe } from '@vueuse/core'
4
+ import { isDrawing } from '../logic/drawings'
5
+ import { next, nextSlide, prev, prevSlide } from '../logic/nav'
6
+
7
+ export function useSwipeControls(root: Ref<HTMLElement | undefined>) {
8
+ const swipeBegin = ref(0)
9
+ const { direction, distanceX, distanceY } = usePointerSwipe(root, {
10
+ pointerTypes: ['touch'],
11
+ onSwipeStart() {
12
+ if (isDrawing.value)
13
+ return
14
+ swipeBegin.value = timestamp()
15
+ },
16
+ onSwipeEnd() {
17
+ if (!swipeBegin.value)
18
+ return
19
+ if (isDrawing.value)
20
+ return
21
+
22
+ const x = Math.abs(distanceX.value)
23
+ const y = Math.abs(distanceY.value)
24
+ if (x / window.innerWidth > 0.3 || x > 75) {
25
+ if (direction.value === 'left')
26
+ next()
27
+
28
+ else
29
+ prev()
30
+ }
31
+ else if (y / window.innerHeight > 0.4 || y > 200) {
32
+ if (direction.value === 'down')
33
+ prevSlide()
34
+
35
+ else
36
+ nextSlide()
37
+ }
38
+ },
39
+ })
40
+ }
@@ -0,0 +1,63 @@
1
+ import type { SlideRoute, TocItem } from '@slidev/types'
2
+ import type { ComputedRef, Ref } from 'vue'
3
+ import { computed } from 'vue'
4
+ import { currentSlideNo, currentSlideRoute, getSlidePath } from '../logic/nav'
5
+
6
+ function addToTree(tree: TocItem[], route: SlideRoute, level = 1) {
7
+ const titleLevel = route.meta?.slide?.level
8
+ if (titleLevel && titleLevel > level && tree.length > 0) {
9
+ addToTree(tree[tree.length - 1].children, route, level + 1)
10
+ }
11
+ else {
12
+ tree.push({
13
+ no: route.no,
14
+ children: [],
15
+ level,
16
+ path: getSlidePath(route.meta.slide?.frontmatter?.routeAlias ?? route.no),
17
+ hideInToc: Boolean(route.meta?.slide?.frontmatter?.hideInToc),
18
+ title: route.meta?.slide?.title,
19
+ })
20
+ }
21
+ }
22
+
23
+ function getTreeWithActiveStatuses(
24
+ tree: TocItem[],
25
+ currentRoute?: SlideRoute,
26
+ hasActiveParent = false,
27
+ parent?: TocItem,
28
+ ): TocItem[] {
29
+ return tree.map((item: TocItem) => {
30
+ const clone = {
31
+ ...item,
32
+ active: item.no === currentSlideNo.value,
33
+ hasActiveParent,
34
+ }
35
+ if (clone.children.length > 0)
36
+ clone.children = getTreeWithActiveStatuses(clone.children, currentRoute, clone.active || clone.hasActiveParent, clone)
37
+ if (parent && (clone.active || clone.activeParent))
38
+ parent.activeParent = true
39
+ return clone
40
+ })
41
+ }
42
+
43
+ function filterTree(tree: TocItem[], level = 1): TocItem[] {
44
+ return tree
45
+ .filter((item: TocItem) => !item.hideInToc)
46
+ .map((item: TocItem) => ({
47
+ ...item,
48
+ children: filterTree(item.children, level + 1),
49
+ }))
50
+ }
51
+
52
+ export function useTocTree(slides: Ref<SlideRoute[]>): ComputedRef<TocItem[]> {
53
+ const rawTree = computed(() => slides.value
54
+ .filter((route: SlideRoute) => route.meta?.slide?.title)
55
+ .reduce((acc: TocItem[], route: SlideRoute) => {
56
+ addToTree(acc, route)
57
+ return acc
58
+ }, []))
59
+
60
+ const treeWithActiveStatuses = computed(() => getTreeWithActiveStatuses(rawTree.value, currentSlideRoute.value))
61
+
62
+ return computed(() => filterTree(treeWithActiveStatuses.value))
63
+ }
package/constants.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import type { ComputedRef, InjectionKey, Ref, UnwrapNestedRefs } from 'vue'
2
- import type { RouteRecordRaw } from 'vue-router'
3
- import type { ClicksContext, RenderContext } from '@slidev/types'
2
+ import type { ClicksContext, RenderContext, SlideRoute } from '@slidev/types'
4
3
  import type { SlidevContext } from './modules/context'
5
4
 
6
5
  // Here we use string literal instead of symbols to make HMR more stable
@@ -9,7 +8,7 @@ export const injectionClicksContext = '$$slidev-clicks-context' as unknown as In
9
8
  export const injectionCurrentPage = '$$slidev-page' as unknown as InjectionKey<Ref<number>>
10
9
  export const injectionSlideScale = '$$slidev-slide-scale' as unknown as InjectionKey<ComputedRef<number>>
11
10
  export const injectionSlidevContext = '$$slidev-context' as unknown as InjectionKey<UnwrapNestedRefs<SlidevContext>>
12
- export const injectionRoute = '$$slidev-route' as unknown as InjectionKey<RouteRecordRaw>
11
+ export const injectionRoute = '$$slidev-route' as unknown as InjectionKey<SlideRoute>
13
12
  export const injectionRenderContext = '$$slidev-render-context' as unknown as InjectionKey<Ref<RenderContext>>
14
13
  export const injectionActive = '$$slidev-active' as unknown as InjectionKey<Ref<boolean>>
15
14
  export const injectionFrontmatter = '$$slidev-fontmatter' as unknown as InjectionKey<Record<string, any>>