@slidev/client 0.49.0-beta.1 → 0.49.0-beta.3

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.
@@ -63,10 +63,10 @@ onMounted(() => {
63
63
  if (!clicks || !props.ranges?.length)
64
64
  return
65
65
 
66
- const { start, end, delta } = clicks.resolve(props.at, props.ranges.length - 1)
67
- clicks.register(id, { max: end, delta })
66
+ const clicksInfo = clicks.calculateSince(props.at, props.ranges.length - 1)
67
+ clicks.register(id, clicksInfo)
68
68
 
69
- const index = computed(() => Math.max(0, clicks.current - start + 1))
69
+ const index = computed(() => Math.max(0, clicks.current - clicksInfo.start + 1))
70
70
 
71
71
  const finallyRange = computed(() => {
72
72
  return props.finally === 'last' ? props.ranges.at(-1) : props.finally.toString()
@@ -58,10 +58,10 @@ onMounted(() => {
58
58
  if (!clicks || !props.ranges?.length)
59
59
  return
60
60
 
61
- const { start, end, delta } = clicks.resolve(props.at, props.ranges.length - 1)
62
- clicks.register(id, { max: end, delta })
61
+ const clicksInfo = clicks.calculateSince(props.at, props.ranges.length - 1)
62
+ clicks.register(id, clicksInfo)
63
63
 
64
- const index = computed(() => Math.max(0, clicks.current - start + 1))
64
+ const index = computed(() => Math.max(0, clicks.current - clicksInfo.start + 1))
65
65
 
66
66
  const finallyRange = computed(() => {
67
67
  return props.finally === 'last' ? props.ranges.at(-1) : props.finally.toString()
@@ -16,7 +16,11 @@ import { debounce } from '@antfu/utils'
16
16
  import lz from 'lz-string'
17
17
  import type * as monaco from 'monaco-editor'
18
18
  import { computed, nextTick, onMounted, ref } from 'vue'
19
+ import type { RawAtValue } from '@slidev/types'
20
+ import { whenever } from '@vueuse/core'
19
21
  import { makeId } from '../logic/utils'
22
+ import { useSlideContext } from '../context'
23
+ import { useNav } from '../composables/useNav'
20
24
  import CodeRunner from '../internals/CodeRunner.vue'
21
25
 
22
26
  const props = withDefaults(defineProps<{
@@ -30,6 +34,7 @@ const props = withDefaults(defineProps<{
30
34
  ata?: boolean
31
35
  runnable?: boolean
32
36
  autorun?: boolean | 'once'
37
+ showOutputAt?: RawAtValue
33
38
  outputHeight?: string
34
39
  highlightOutput?: boolean
35
40
  runnerOptions?: Record<string, unknown>
@@ -74,6 +79,19 @@ const height = computed(() => {
74
79
  return props.height
75
80
  })
76
81
 
82
+ const loadTypes = ref<() => void>()
83
+ const { $page: thisSlideNo, $renderContext: renderContext } = useSlideContext()
84
+ const { currentSlideNo } = useNav()
85
+ const stopWatchTypesLoading = whenever(
86
+ () => Math.abs(thisSlideNo.value - currentSlideNo.value) <= 1 && loadTypes.value,
87
+ (loadTypes) => {
88
+ if (['slide', 'presenter'].includes(renderContext.value))
89
+ loadTypes()
90
+ else
91
+ setTimeout(loadTypes, 5000)
92
+ },
93
+ )
94
+
77
95
  onMounted(async () => {
78
96
  // Lazy load monaco, so it will be bundled in async chunk
79
97
  const { default: setup } = await import('../setup/monaco')
@@ -135,11 +153,15 @@ onMounted(async () => {
135
153
  })
136
154
  editableEditor = editor
137
155
  }
138
- if (props.ata) {
139
- ata(editableEditor.getValue())
140
- editableEditor.onDidChangeModelContent(debounce(1000, () => {
156
+ loadTypes.value = () => {
157
+ stopWatchTypesLoading()
158
+ import('#slidev/monaco-types')
159
+ if (props.ata) {
141
160
  ata(editableEditor.getValue())
142
- }))
161
+ editableEditor.onDidChangeModelContent(debounce(1000, () => {
162
+ ata(editableEditor.getValue())
163
+ }))
164
+ }
143
165
  }
144
166
  const originalLayoutContentWidget = editableEditor.layoutContentWidget.bind(editableEditor)
145
167
  editableEditor.layoutContentWidget = (widget: any) => {
@@ -165,6 +187,7 @@ onMounted(async () => {
165
187
  v-model="code"
166
188
  :lang="lang"
167
189
  :autorun="props.autorun"
190
+ :show-output-at="props.showOutputAt"
168
191
  :height="props.outputHeight"
169
192
  :highlight-output="props.highlightOutput"
170
193
  :runner-options="props.runnerOptions"
@@ -36,14 +36,14 @@ onMounted(() => {
36
36
  throw new Error('[slidev] The length of stepRanges does not match the length of steps, this is an internal error.')
37
37
 
38
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)
40
- clicks.register(id, { max: end, delta })
39
+ const clickInfo = clicks.calculateSince(props.at ?? '+1', clickCounts - 1)
40
+ clicks.register(id, clickInfo)
41
41
 
42
42
  watch(
43
43
  () => clicks.current,
44
44
  () => {
45
45
  // Calculate the step and rangeStr based on the current click count
46
- const clickCount = clicks.current - start
46
+ const clickCount = clicks.current - clickInfo.start
47
47
  let step = steps.length - 1
48
48
  let currentClickSum = 0
49
49
  let rangeStr = 'all'
@@ -2,6 +2,7 @@
2
2
  import { computed, onMounted, ref, watch } from 'vue'
3
3
  import { and } from '@vueuse/math'
4
4
  import { useSlideContext } from '../context'
5
+ import { resolvedClickMap } from '../modules/v-click'
5
6
  import { useNav } from '../composables/useNav'
6
7
 
7
8
  const props = defineProps<{
@@ -17,12 +18,7 @@ const props = defineProps<{
17
18
  const printPoster = computed(() => props.printPoster ?? props.poster)
18
19
  const printTimestamp = computed(() => props.printTimestamp ?? props.timestamp ?? 0)
19
20
 
20
- const {
21
- $slidev,
22
- $clicksContext,
23
- $renderContext,
24
- $route,
25
- } = useSlideContext()
21
+ const { $slidev, $renderContext, $route } = useSlideContext()
26
22
  const { isPrintMode } = useNav()
27
23
 
28
24
  const noPlay = computed(() => isPrintMode.value || !['slide', 'presenter'].includes($renderContext.value))
@@ -38,7 +34,7 @@ onMounted(() => {
38
34
  video.value!.currentTime = timestamp
39
35
 
40
36
  const matchRoute = computed(() => !!$route && $route.no === $slidev?.nav.currentSlideNo)
41
- const matchClick = computed(() => !!video.value && ($clicksContext.map.get(video.value)?.isShown?.value ?? true))
37
+ const matchClick = computed(() => !!video.value && (resolvedClickMap.get(video.value)?.isShown?.value ?? true))
42
38
  const matchRouteAndClick = and(matchRoute, matchClick)
43
39
 
44
40
  watch(matchRouteAndClick, () => {
@@ -7,7 +7,7 @@
7
7
  import { toArray } from '@antfu/utils'
8
8
  import type { VNode, VNodeArrayChildren } from 'vue'
9
9
  import { Comment, createVNode, defineComponent, h, isVNode, resolveDirective, withDirectives } from 'vue'
10
- import { normalizeAtProp } from '../logic/utils'
10
+ import { normalizeAtValue } from '../composables/useClicks'
11
11
  import VClickGap from './VClickGap.vue'
12
12
 
13
13
  const listTags = ['ul', 'ol']
@@ -37,7 +37,12 @@ export default defineComponent({
37
37
  },
38
38
  render() {
39
39
  const every = +this.every
40
- const [isRelative, at] = normalizeAtProp(this.at)
40
+ const at = normalizeAtValue(this.at)
41
+ const isRelative = typeof at === 'string'
42
+ if (typeof at !== 'string' && typeof at !== 'number') {
43
+ console.warn('[slidev] Invalid at prop for v-clicks component:', at)
44
+ return
45
+ }
41
46
 
42
47
  const click = resolveDirective('click')!
43
48
 
@@ -1,67 +1,115 @@
1
1
  import { clamp, sum } from '@antfu/utils'
2
- import type { ClicksContext, SlideRoute } from '@slidev/types'
2
+ import type { ClicksContext, NormalizedAtValue, RawAtValue, SlideRoute } from '@slidev/types'
3
3
  import type { Ref } from 'vue'
4
- import { ref, shallowReactive } from 'vue'
5
- import { normalizeAtProp } from '../logic/utils'
4
+ import { computed, ref, shallowReactive } from 'vue'
6
5
  import { routeForceRefresh } from '../logic/route'
7
6
 
7
+ export function normalizeAtValue(at: RawAtValue): NormalizedAtValue {
8
+ if (at === false || at === 'false')
9
+ return null
10
+ if (at == null || at === true || at === 'true')
11
+ return '+1'
12
+ if (Array.isArray(at))
13
+ return [+at[0], +at[1]]
14
+ if (typeof at === 'string' && '+-'.includes(at[0]))
15
+ return at
16
+ return +at
17
+ }
18
+
8
19
  export function createClicksContextBase(
9
20
  current: Ref<number>,
10
21
  clicksStart = 0,
11
22
  clicksTotalOverrides?: number,
12
23
  ): ClicksContext {
13
- const relativeOffsets: ClicksContext['relativeOffsets'] = new Map()
14
- const map: ClicksContext['map'] = shallowReactive(new Map())
15
-
16
- return {
24
+ const context: ClicksContext = {
17
25
  get current() {
18
26
  // Here we haven't know clicksTotal yet.
19
- return clamp(+current.value, clicksStart, this.total)
27
+ return clamp(+current.value, clicksStart, context.total)
20
28
  },
21
29
  set current(value) {
22
- current.value = clamp(+value, clicksStart, this.total)
30
+ current.value = clamp(+value, clicksStart, context.total)
23
31
  },
24
32
  clicksStart,
25
- relativeOffsets,
26
- map,
33
+ relativeOffsets: new Map(),
34
+ maxMap: shallowReactive(new Map()),
27
35
  onMounted() { },
28
- resolve(at, size = 1) {
29
- const [isRelative, value] = normalizeAtProp(at)
30
- if (isRelative) {
31
- const offset = this.currentOffset
32
- return {
33
- start: offset + value,
34
- end: offset + value + size - 1,
35
- delta: value + size - 1,
36
- }
36
+ calculateSince(at, size = 1) {
37
+ let start: number, max: number, delta: number
38
+ if (typeof at === 'string') {
39
+ const offset = context.currentOffset
40
+ const value = +at
41
+ start = offset + value
42
+ max = offset + value + size - 1
43
+ delta = value + size - 1
37
44
  }
38
45
  else {
39
- return {
40
- start: value,
41
- end: value + size - 1,
42
- delta: 0,
43
- }
46
+ start = at
47
+ max = at + size - 1
48
+ delta = 0
49
+ }
50
+ return {
51
+ start,
52
+ end: +Number.POSITIVE_INFINITY,
53
+ max,
54
+ delta,
55
+ isCurrent: computed(() => context.current === start),
56
+ isActive: computed(() => context.current >= start),
44
57
  }
45
58
  },
46
- register(el, resolved) {
47
- relativeOffsets.set(el, resolved.delta)
48
- map.set(el, resolved)
59
+ calculateRange([a, b]) {
60
+ let start: number, end: number, delta: number
61
+ if (typeof a === 'string') {
62
+ const offset = context.currentOffset
63
+ start = offset + +a
64
+ delta = +a
65
+ }
66
+ else {
67
+ start = a
68
+ delta = 0
69
+ }
70
+ if (typeof b === 'string') {
71
+ end = start + +b
72
+ delta += +b
73
+ }
74
+ else {
75
+ end = b
76
+ }
77
+ return {
78
+ start,
79
+ end,
80
+ max: end,
81
+ delta,
82
+ isCurrent: computed(() => context.current === start),
83
+ isActive: computed(() => start <= context.current && context.current < end),
84
+ }
85
+ },
86
+ calculate(at) {
87
+ if (at == null)
88
+ return null
89
+ if (Array.isArray(at))
90
+ return context.calculateRange(at)
91
+ return context.calculateSince(at)
92
+ },
93
+ register(el, { delta, max }) {
94
+ context.relativeOffsets.set(el, delta)
95
+ context.maxMap.set(el, max)
49
96
  },
50
97
  unregister(el) {
51
- relativeOffsets.delete(el)
52
- map.delete(el)
98
+ context.relativeOffsets.delete(el)
99
+ context.maxMap.delete(el)
53
100
  },
54
101
  get currentOffset() {
55
102
  // eslint-disable-next-line no-unused-expressions
56
103
  routeForceRefresh.value
57
- return sum(...relativeOffsets.values())
104
+ return sum(...context.relativeOffsets.values())
58
105
  },
59
106
  get total() {
60
107
  // eslint-disable-next-line no-unused-expressions
61
108
  routeForceRefresh.value
62
- return clicksTotalOverrides ?? Math.max(0, ...[...map.values()].map(v => v.max || 0))
109
+ return clicksTotalOverrides ?? Math.max(0, ...context.maxMap.values())
63
110
  },
64
111
  }
112
+ return context
65
113
  }
66
114
 
67
115
  export function createFixedClicks(
@@ -52,7 +52,9 @@ export function useDragElementsUpdater(no: number) {
52
52
  return
53
53
  frontmatter.dragPos[id] = posStr
54
54
  newPatch = {
55
- frontmatter,
55
+ frontmatter: {
56
+ dragPos: frontmatter.dragPos,
57
+ },
56
58
  }
57
59
  }
58
60
  else {
@@ -267,8 +269,10 @@ export function useDragElement(directive: DirectiveBinding | null, posRaw?: stri
267
269
 
268
270
  watchStopHandles.push(
269
271
  onClickOutside(container, (ev) => {
270
- if ((ev.target as HTMLElement | null)?.dataset?.dragId !== id)
271
- state.stopDragging()
272
+ const container = document.querySelector('#drag-control-container')
273
+ if (container && ev.target && container.contains(ev.target as HTMLElement))
274
+ return
275
+ state.stopDragging()
272
276
  }),
273
277
  watch(useWindowFocus(), (focused) => {
274
278
  if (!focused)
@@ -60,6 +60,11 @@ export interface SlidevContextNav {
60
60
  goFirst: () => Promise<void>
61
61
  /** Go to the last slide */
62
62
  goLast: () => Promise<void>
63
+
64
+ /** Enter presenter mode */
65
+ enterPresenter: () => void
66
+ /** Exit presenter mode */
67
+ exitPresenter: () => void
63
68
  }
64
69
 
65
70
  export interface SlidevContextNavState {
@@ -194,6 +199,19 @@ export function useNavBase(
194
199
  }
195
200
  }
196
201
 
202
+ function enterPresenter() {
203
+ router?.push({
204
+ path: getSlidePath(currentSlideNo.value, true),
205
+ query: { ...router.currentRoute.value.query },
206
+ })
207
+ }
208
+ function exitPresenter() {
209
+ router?.push({
210
+ path: getSlidePath(currentSlideNo.value, false),
211
+ query: { ...router.currentRoute.value.query },
212
+ })
213
+ }
214
+
197
215
  return {
198
216
  slides,
199
217
  total,
@@ -222,6 +240,8 @@ export function useNavBase(
222
240
  goFirst,
223
241
  nextSlide,
224
242
  prevSlide,
243
+ enterPresenter,
244
+ exitPresenter,
225
245
  }
226
246
  }
227
247
 
package/constants.ts CHANGED
@@ -14,7 +14,6 @@ export const injectionRenderContext = '$$slidev-render-context' as unknown as In
14
14
  export const injectionActive = '$$slidev-active' as unknown as InjectionKey<Ref<boolean>>
15
15
  export const injectionFrontmatter = '$$slidev-fontmatter' as unknown as InjectionKey<Record<string, any>>
16
16
  export const injectionSlideZoom = '$$slidev-slide-zoom' as unknown as InjectionKey<ComputedRef<number>>
17
- export const injectionClickVisibility = '$$slidev-click-visibility' as unknown as InjectionKey<ComputedRef<true | 'before' | 'after'>>
18
17
 
19
18
  export const CLASS_VCLICK_TARGET = 'slidev-vclick-target'
20
19
  export const CLASS_VCLICK_HIDDEN = 'slidev-vclick-hidden'
@@ -77,4 +76,5 @@ export const HEADMATTER_FIELDS = [
77
76
  'drawings',
78
77
  'htmlAttrs',
79
78
  'mdc',
79
+ 'contextMenu',
80
80
  ]
package/env.ts CHANGED
@@ -4,6 +4,8 @@ import configs from '#slidev/configs'
4
4
 
5
5
  export { configs }
6
6
 
7
+ export const mode = __DEV__ ? 'dev' : 'build'
8
+
7
9
  export const slideAspect = ref(configs.aspectRatio ?? (16 / 9))
8
10
  export const slideWidth = ref(configs.canvasWidth ?? 980)
9
11
 
@@ -1,11 +1,13 @@
1
1
  <script setup lang="ts">
2
2
  import { debounce, toArray } from '@antfu/utils'
3
3
  import { useVModel } from '@vueuse/core'
4
- import type { CodeRunnerOutput } from '@slidev/types'
5
- import { computed, ref, shallowRef, watch } from 'vue'
4
+ import type { CodeRunnerOutput, RawAtValue } from '@slidev/types'
5
+ import { computed, onMounted, onUnmounted, ref, shallowRef, watch, watchSyncEffect } from 'vue'
6
6
  import { useSlideContext } from '../context'
7
7
  import setupCodeRunners from '../setup/code-runners'
8
8
  import { useNav } from '../composables/useNav'
9
+ import { makeId } from '../logic/utils'
10
+ import { normalizeAtValue } from '../composables/useClicks'
9
11
  import IconButton from './IconButton.vue'
10
12
  import DomElement from './DomElement.vue'
11
13
 
@@ -14,6 +16,7 @@ const props = defineProps<{
14
16
  lang: string
15
17
  autorun: boolean | 'once'
16
18
  height?: string
19
+ showOutputAt?: RawAtValue
17
20
  highlightOutput: boolean
18
21
  runnerOptions?: Record<string, unknown>
19
22
  }>()
@@ -24,7 +27,7 @@ const { isPrintMode } = useNav()
24
27
 
25
28
  const code = useVModel(props, 'modelValue', emit)
26
29
 
27
- const { $renderContext } = useSlideContext()
30
+ const { $renderContext, $clicksContext } = useSlideContext()
28
31
  const disabled = computed(() => !['slide', 'presenter'].includes($renderContext.value))
29
32
 
30
33
  const autorun = isPrintMode.value ? 'once' : props.autorun
@@ -33,6 +36,25 @@ const outputs = shallowRef<CodeRunnerOutput[]>()
33
36
  const runCount = ref(0)
34
37
  const highlightFn = ref<(code: string, lang: string) => string>()
35
38
 
39
+ const hidden = ref(props.showOutputAt)
40
+ if (props.showOutputAt) {
41
+ const id = makeId()
42
+ onMounted(() => {
43
+ const at = normalizeAtValue(props.showOutputAt)
44
+ const info = $clicksContext.calculate(at)
45
+ if (info) {
46
+ $clicksContext.register(id, info)
47
+ watchSyncEffect(() => {
48
+ hidden.value = !info.isActive.value
49
+ })
50
+ }
51
+ else {
52
+ hidden.value = false
53
+ }
54
+ })
55
+ onUnmounted(() => $clicksContext.unregister(id))
56
+ }
57
+
36
58
  const triggerRun = debounce(200, async () => {
37
59
  if (disabled.value)
38
60
  return
@@ -59,6 +81,7 @@ else if (autorun)
59
81
 
60
82
  <template>
61
83
  <div
84
+ v-show="!hidden"
62
85
  class="relative flex flex-col rounded-b border-t border-main"
63
86
  :style="{ height: props.height }"
64
87
  data-waitfor=".slidev-runner-output"
@@ -0,0 +1,110 @@
1
+ <script setup lang="ts">
2
+ import { onClickOutside, useElementBounding, useEventListener, useWindowFocus } from '@vueuse/core'
3
+ import { computed, ref, watch } from 'vue'
4
+ import { closeContextMenu, currentContextMenu } from '../logic/contextMenu'
5
+ import { useDynamicSlideInfo } from '../composables/useSlideInfo'
6
+ import { windowSize } from '../state'
7
+ import { configs } from '../env'
8
+
9
+ const container = ref<HTMLElement>()
10
+
11
+ onClickOutside(container, closeContextMenu)
12
+ useEventListener(document, 'mousedown', (ev) => {
13
+ if (ev.buttons & 2)
14
+ closeContextMenu()
15
+ }, {
16
+ passive: true,
17
+ capture: true,
18
+ })
19
+
20
+ const isExplicitEnabled = computed(() => configs.contextMenu != null)
21
+
22
+ const windowFocus = useWindowFocus()
23
+ watch(windowFocus, (hasFocus) => {
24
+ if (!hasFocus)
25
+ closeContextMenu()
26
+ })
27
+
28
+ const firstSlide = useDynamicSlideInfo(1)
29
+ function disableContextMenu() {
30
+ const info = firstSlide.info.value
31
+ if (!info)
32
+ return
33
+ firstSlide.update({
34
+ frontmatter: {
35
+ contextMenu: false,
36
+ },
37
+ })
38
+ }
39
+
40
+ const { width, height } = useElementBounding(container)
41
+ const left = computed(() => {
42
+ const x = currentContextMenu.value?.x
43
+ if (!x)
44
+ return 0
45
+ if (x + width.value > windowSize.width.value)
46
+ return windowSize.width.value - width.value
47
+ return x
48
+ })
49
+ const top = computed(() => {
50
+ const y = currentContextMenu.value?.y
51
+ if (!y)
52
+ return 0
53
+ if (y + height.value > windowSize.height.value)
54
+ return windowSize.height.value - height.value
55
+ return y
56
+ })
57
+ </script>
58
+
59
+ <template>
60
+ <div
61
+ v-if="currentContextMenu"
62
+ ref="container"
63
+ :style="`left:${left}px;top:${top}px`"
64
+ class="fixed z-100 w-60 flex flex-wrap justify-items-start p-1 animate-fade-in animate-duration-100 backdrop-blur bg-main bg-opacity-75! border border-main rounded-md shadow overflow-hidden select-none"
65
+ @contextmenu.prevent=""
66
+ @click="closeContextMenu"
67
+ >
68
+ <template v-for="item, index of currentContextMenu.items.value" :key="index">
69
+ <div v-if="item === 'separator'" :key="index" class="w-full my1 border-t border-main" />
70
+ <div
71
+ v-else-if="item.small"
72
+ class="p-2 w-[40px] h-[40px] inline-block text-center cursor-pointer rounded"
73
+ :class="item.disabled ? `op40` : `hover:bg-active`"
74
+ :title="(item.label as string)"
75
+ @click="item.action"
76
+ >
77
+ <component :is="item.icon" />
78
+ </div>
79
+ <div
80
+ v-else
81
+ class="w-full grid grid-cols-[35px_1fr] p-2 pl-0 cursor-pointer rounded"
82
+ :class="item.disabled ? `op40` : `hover:bg-active`"
83
+ @click="item.action"
84
+ >
85
+ <div class="mx-auto">
86
+ <component :is="item.icon" />
87
+ </div>
88
+ <div v-if="typeof item.label === 'string'">
89
+ {{ item.label }}
90
+ </div>
91
+ <component :is="item.label" v-else />
92
+ </div>
93
+ </template>
94
+ <template v-if="!isExplicitEnabled">
95
+ <div class="w-full my1 border-t border-main" />
96
+ <div class="w-full text-xs p2">
97
+ <div class="text-main text-opacity-50!">
98
+ Hold <kbd class="border px1 py0.5 border-main rounded text-primary">Shift</kbd> and right click to open the native context menu
99
+ <button
100
+ v-if="__DEV__"
101
+ class="underline op50 hover:op100 mt1 block"
102
+ @click="disableContextMenu()"
103
+ >
104
+ Disable custom context menu
105
+ </button>
106
+ </div>
107
+ </div>
108
+ </template>
109
+ </div>
110
+ </template>
@@ -5,6 +5,7 @@ import { configs } from '../env'
5
5
  import QuickOverview from './QuickOverview.vue'
6
6
  import InfoDialog from './InfoDialog.vue'
7
7
  import Goto from './Goto.vue'
8
+ import ContextMenu from './ContextMenu.vue'
8
9
 
9
10
  const WebCamera = shallowRef<any>()
10
11
  const RecordingDialog = shallowRef<any>()
@@ -20,4 +21,5 @@ if (__SLIDEV_FEATURE_RECORD__) {
20
21
  <WebCamera v-if="WebCamera" />
21
22
  <RecordingDialog v-if="RecordingDialog" v-model="showRecordingDialog" />
22
23
  <InfoDialog v-if="configs.info" v-model="showInfoDialog" />
24
+ <ContextMenu />
23
25
  </template>
@@ -355,6 +355,7 @@ watchEffect(() => {
355
355
  <template>
356
356
  <div
357
357
  v-if="Number.isFinite(x0)"
358
+ id="drag-control-container"
358
359
  :data-drag-id="id"
359
360
  :style="{
360
361
  position: 'absolute',
@@ -5,7 +5,6 @@ import { downloadPDF } from '../utils'
5
5
  import { activeElement, breakpoints, fullscreen, presenterLayout, showEditor, showInfoDialog, showPresenterCursor, toggleOverview, togglePresenterLayout } from '../state'
6
6
  import { configs } from '../env'
7
7
  import { useNav } from '../composables/useNav'
8
- import { getSlidePath } from '../logic/slides'
9
8
  import { useDrawings } from '../composables/useDrawings'
10
9
  import Settings from './Settings.vue'
11
10
  import MenuButton from './MenuButton.vue'
@@ -21,7 +20,6 @@ const props = defineProps({
21
20
  })
22
21
 
23
22
  const {
24
- currentRoute,
25
23
  currentSlideNo,
26
24
  hasNext,
27
25
  hasPrev,
@@ -31,6 +29,8 @@ const {
31
29
  next,
32
30
  prev,
33
31
  total,
32
+ enterPresenter,
33
+ exitPresenter,
34
34
  } = useNav()
35
35
  const {
36
36
  brush,
@@ -40,11 +40,6 @@ const {
40
40
  const md = breakpoints.smaller('md')
41
41
  const { isFullscreen, toggle: toggleFullscreen } = fullscreen
42
42
 
43
- const presenterPassword = computed(() => currentRoute.value.query.password)
44
- const query = computed(() => presenterPassword.value ? `?password=${presenterPassword.value}` : '')
45
- const presenterLink = computed(() => `${getSlidePath(currentSlideNo.value, true)}${query.value}`)
46
- const nonPresenterLink = computed(() => `${getSlidePath(currentSlideNo.value, false)}${query.value}`)
47
-
48
43
  const root = ref<HTMLDivElement>()
49
44
  function onMouseLeave() {
50
45
  if (root.value && activeElement.value && root.value.contains(activeElement.value))
@@ -124,12 +119,12 @@ if (__SLIDEV_FEATURE_DRAWINGS__)
124
119
  </template>
125
120
 
126
121
  <template v-if="!isEmbedded">
127
- <RouterLink v-if="isPresenter" :to="nonPresenterLink" class="slidev-icon-btn" title="Play Mode">
122
+ <IconButton v-if="isPresenter" title="Play Mode" @click="exitPresenter">
128
123
  <carbon:presentation-file />
129
- </RouterLink>
130
- <RouterLink v-if="__SLIDEV_FEATURE_PRESENTER__ && isPresenterAvailable" :to="presenterLink" class="slidev-icon-btn" title="Presenter Mode">
124
+ </IconButton>
125
+ <IconButton v-if="__SLIDEV_FEATURE_PRESENTER__ && isPresenterAvailable" title="Presenter Mode" @click="enterPresenter">
131
126
  <carbon:user-speaker />
132
- </RouterLink>
127
+ </IconButton>
133
128
 
134
129
  <IconButton
135
130
  v-if="__DEV__ && __SLIDEV_FEATURE_EDITOR__"