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

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,6 +16,7 @@ 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'
19
20
  import { makeId } from '../logic/utils'
20
21
  import CodeRunner from '../internals/CodeRunner.vue'
21
22
 
@@ -30,6 +31,7 @@ const props = withDefaults(defineProps<{
30
31
  ata?: boolean
31
32
  runnable?: boolean
32
33
  autorun?: boolean | 'once'
34
+ showOutputAt?: RawAtValue
33
35
  outputHeight?: string
34
36
  highlightOutput?: boolean
35
37
  runnerOptions?: Record<string, unknown>
@@ -165,6 +167,7 @@ onMounted(async () => {
165
167
  v-model="code"
166
168
  :lang="lang"
167
169
  :autorun="props.autorun"
170
+ :show-output-at="props.showOutputAt"
168
171
  :height="props.outputHeight"
169
172
  :highlight-output="props.highlightOutput"
170
173
  :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__"
@@ -0,0 +1,34 @@
1
+ import type { ContextMenuItem } from '@slidev/types'
2
+ import type { ComputedRef } from 'vue'
3
+ import { shallowRef } from 'vue'
4
+ import setupContextMenu from '../setup/context-menu'
5
+ import { configs, mode } from '../env'
6
+
7
+ export const currentContextMenu = shallowRef<null | {
8
+ x: number
9
+ y: number
10
+ items: ComputedRef<ContextMenuItem[]>
11
+ }>(null)
12
+
13
+ export function openContextMenu(x: number, y: number) {
14
+ currentContextMenu.value = {
15
+ x,
16
+ y,
17
+ items: setupContextMenu(),
18
+ }
19
+ }
20
+
21
+ export function closeContextMenu() {
22
+ currentContextMenu.value = null
23
+ }
24
+
25
+ export function onContextMenu(ev: MouseEvent) {
26
+ if (configs.contextMenu !== true && configs.contextMenu !== undefined && configs.contextMenu !== mode)
27
+ return
28
+ if (ev.shiftKey || ev.defaultPrevented)
29
+ return
30
+
31
+ openContextMenu(ev.pageX, ev.pageY)
32
+ ev.preventDefault()
33
+ ev.stopPropagation()
34
+ }
package/logic/utils.ts CHANGED
@@ -32,24 +32,6 @@ export function makeId(length = 5) {
32
32
  return result.join('')
33
33
  }
34
34
 
35
- /**
36
- * '+3' => '+3'
37
- * '-3' => '-3'
38
- * '3' => 3
39
- * 3 => 3
40
- */
41
- export function normalizeAtProp(at: string | number = '+1'): [isRelative: boolean, value: number] {
42
- let n = +at
43
- if (Number.isNaN(n)) {
44
- console.warn('[slidev] Invalid click position:', at)
45
- n = 0
46
- }
47
- return [
48
- typeof at === 'string' && '+-'.includes(at[0]),
49
- n,
50
- ]
51
- }
52
-
53
35
  export function updateCodeHighlightRange(
54
36
  rangeStr: string,
55
37
  linesCount: number,
@@ -1,6 +1,6 @@
1
- import type { ResolvedClicksInfo } from '@slidev/types'
2
1
  import type { App, DirectiveBinding } from 'vue'
3
2
  import { computed, watchEffect } from 'vue'
3
+ import type { ClicksElement, RawAtValue } from '@slidev/types'
4
4
  import {
5
5
  CLASS_VCLICK_CURRENT,
6
6
  CLASS_VCLICK_FADE,
@@ -8,32 +8,29 @@ import {
8
8
  CLASS_VCLICK_HIDDEN_EXP,
9
9
  CLASS_VCLICK_PRIOR,
10
10
  CLASS_VCLICK_TARGET,
11
- injectionClickVisibility,
12
11
  injectionClicksContext,
13
12
  } from '../constants'
14
- import { directiveInject, directiveProvide } from '../utils'
15
-
16
- export type VClickValue = undefined | string | number | [string | number, string | number] | boolean
13
+ import { directiveInject } from '../utils'
14
+ import { normalizeAtValue } from '../composables/useClicks'
17
15
 
18
16
  export function createVClickDirectives() {
19
17
  return {
20
18
  install(app: App) {
21
- app.directive<HTMLElement, VClickValue>('click', {
19
+ app.directive<HTMLElement, RawAtValue>('click', {
22
20
  // @ts-expect-error extra prop
23
21
  name: 'v-click',
24
22
 
25
23
  mounted(el, dir) {
26
- const resolved = resolveClick(el, dir, dir.value, true)
24
+ const resolved = resolveClick(el, dir, dir.value)
27
25
  if (resolved == null)
28
26
  return
29
27
 
30
28
  el.classList.toggle(CLASS_VCLICK_TARGET, true)
31
29
 
32
30
  // Expose the resolved clicks info to the element to make it easier to understand and debug
33
- const clicks = Array.isArray(resolved.clicks) ? resolved.clicks : [resolved.clicks, undefined]
34
- el.dataset.slidevClicksStart = String(clicks[0])
35
- if (clicks[1] != null)
36
- el.dataset.slidevClicksEnd = String(clicks[1])
31
+ el.dataset.slidevClicksStart = String(resolved.start)
32
+ if (Number.isFinite(resolved.end))
33
+ el.dataset.slidevClicksEnd = String(resolved.end)
37
34
 
38
35
  // @ts-expect-error extra prop
39
36
  el.watchStopHandle = watchEffect(() => {
@@ -56,12 +53,12 @@ export function createVClickDirectives() {
56
53
  unmounted,
57
54
  })
58
55
 
59
- app.directive<HTMLElement, VClickValue>('after', {
56
+ app.directive<HTMLElement, RawAtValue>('after', {
60
57
  // @ts-expect-error extra prop
61
58
  name: 'v-after',
62
59
 
63
60
  mounted(el, dir) {
64
- const resolved = resolveClick(el, dir, dir.value, true, true)
61
+ const resolved = resolveClick(el, dir, '+0')
65
62
  if (resolved == null)
66
63
  return
67
64
 
@@ -88,12 +85,12 @@ export function createVClickDirectives() {
88
85
  unmounted,
89
86
  })
90
87
 
91
- app.directive<HTMLElement, VClickValue>('click-hide', {
88
+ app.directive<HTMLElement, RawAtValue>('click-hide', {
92
89
  // @ts-expect-error extra prop
93
90
  name: 'v-click-hide',
94
91
 
95
92
  mounted(el, dir) {
96
- const resolved = resolveClick(el, dir, dir.value, true, false, true)
93
+ const resolved = resolveClick(el, dir, dir.value, true)
97
94
  if (resolved == null)
98
95
  return
99
96
 
@@ -118,75 +115,42 @@ export function createVClickDirectives() {
118
115
  }
119
116
  }
120
117
 
121
- function isClickActive(thisClick: number | [number, number], clicks: number) {
122
- return Array.isArray(thisClick)
123
- ? thisClick[0] <= clicks && clicks < thisClick[1]
124
- : thisClick <= clicks
125
- }
118
+ export const resolvedClickMap = new Map<ClicksElement, ReturnType<typeof resolveClick>>()
126
119
 
127
- function isClickCurrent(thisClick: number | [number, number], clicks: number) {
128
- return Array.isArray(thisClick)
129
- ? thisClick[0] === clicks
130
- : thisClick === clicks
131
- }
132
-
133
- export function resolveClick(el: Element | string, dir: DirectiveBinding<any>, value: VClickValue, provideVisibility = false, clickAfter = false, flagHide = false): ResolvedClicksInfo | null {
120
+ export function resolveClick(el: Element | string, dir: DirectiveBinding<any>, value: RawAtValue, explicitHide = false) {
134
121
  const ctx = directiveInject(dir, injectionClicksContext)?.value
135
122
 
136
123
  if (!el || !ctx)
137
124
  return null
138
125
 
139
- if (value === false || value === 'false')
140
- return null
141
-
142
- flagHide ||= dir.modifiers.hide !== false && dir.modifiers.hide != null
126
+ const flagHide = explicitHide || (dir.modifiers.hide !== false && dir.modifiers.hide != null)
143
127
  const flagFade = dir.modifiers.fade !== false && dir.modifiers.fade != null
144
128
 
145
- if (clickAfter)
146
- value = '+0'
147
- else if (value == null || value === true || value === 'true')
148
- value = '+1'
149
-
150
- let delta: number
151
- let thisClick: number | [number, number]
152
- let maxClick: number
153
- if (Array.isArray(value)) {
154
- // range (absolute)
155
- delta = 0
156
- thisClick = [+value[0], +value[1]]
157
- maxClick = +value[1]
158
- }
159
- else {
160
- ({ start: thisClick, end: maxClick, delta } = ctx.resolve(value))
161
- }
129
+ const at = normalizeAtValue(value)
130
+ const info = ctx.calculate(at)
131
+ if (!info)
132
+ return null
162
133
 
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)
134
+ ctx.register(el, info)
166
135
 
167
- const resolved: ResolvedClicksInfo = {
168
- max: maxClick,
169
- clicks: thisClick,
170
- delta,
171
- isActive,
172
- isCurrent,
136
+ const isShown = computed(() => flagHide ? !info.isActive.value : info.isActive.value)
137
+ const visibilityState = computed(() => {
138
+ if (isShown.value)
139
+ return 'shown'
140
+ if (Number.isFinite(info.end))
141
+ return ctx.current < info.start ? 'before' : 'after'
142
+ else
143
+ return flagHide ? 'after' : 'before'
144
+ })
145
+
146
+ const resolved = {
147
+ ...info,
173
148
  isShown,
149
+ visibilityState,
174
150
  flagFade,
175
151
  flagHide,
176
152
  }
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
-
153
+ resolvedClickMap.set(el, resolved)
190
154
  return resolved
191
155
  }
192
156
 
package/modules/v-mark.ts CHANGED
@@ -2,14 +2,14 @@ import type { RoughAnnotationConfig } from '@slidev/rough-notation'
2
2
  import { annotate } from '@slidev/rough-notation'
3
3
  import type { App } from 'vue'
4
4
  import { computed, watchEffect } from 'vue'
5
- import type { VClickValue } from './v-click'
5
+ import type { RawAtValue } from '@slidev/types'
6
6
  import { resolveClick } from './v-click'
7
7
 
8
8
  export interface RoughDirectiveOptions extends Partial<RoughAnnotationConfig> {
9
- at: VClickValue
9
+ at: RawAtValue
10
10
  }
11
11
 
12
- export type RoughDirectiveValue = VClickValue | RoughDirectiveOptions
12
+ export type RoughDirectiveValue = RawAtValue | RoughDirectiveOptions
13
13
 
14
14
  function addClass(options: RoughDirectiveOptions, cls: string) {
15
15
  options.class = [options.class, cls].filter(Boolean).join(' ')
@@ -1,18 +1,12 @@
1
1
  import type { App, ObjectDirective } from 'vue'
2
2
  import { watch } from 'vue'
3
3
  import { MotionDirective } from '@vueuse/motion'
4
- import type { ResolvedClicksInfo } from '@slidev/types'
5
- import { injectionClickVisibility, injectionClicksContext, injectionCurrentPage, injectionRenderContext } from '../constants'
4
+ import type { ClicksInfo } from '@slidev/types'
5
+ import { injectionClicksContext, injectionCurrentPage, injectionRenderContext } from '../constants'
6
6
  import { useNav } from '../composables/useNav'
7
7
  import { makeId } from '../logic/utils'
8
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
- }
9
+ import { resolvedClickMap } from './v-click'
16
10
 
17
11
  export function createVMotionDirectives() {
18
12
  return {
@@ -22,6 +16,11 @@ export function createVMotionDirectives() {
22
16
  // @ts-expect-error extra prop
23
17
  name: 'v-motion',
24
18
  mounted(el, binding, node, prevNode) {
19
+ const clicksContext = directiveInject(binding, injectionClicksContext)
20
+ const thisPage = directiveInject(binding, injectionCurrentPage)
21
+ const renderContext = directiveInject(binding, injectionRenderContext)
22
+ const { currentPage, clicks: currentClicks, isPrintMode } = useNav()
23
+
25
24
  const props = node.props = { ...node.props }
26
25
 
27
26
  const variantInitial = { ...props.initial, ...props.variants?.['slidev-initial'] }
@@ -36,7 +35,7 @@ export function createVMotionDirectives() {
36
35
  id: string
37
36
  at: number | [number, number]
38
37
  variant: Record<string, unknown>
39
- resolved: ResolvedClicksInfo | null
38
+ info: ClicksInfo | null | undefined
40
39
  }[] = []
41
40
 
42
41
  for (const k of Object.keys(props)) {
@@ -48,7 +47,7 @@ export function createVMotionDirectives() {
48
47
  id,
49
48
  at,
50
49
  variant: { ...props[k] },
51
- resolved: resolveClick(id, binding, at),
50
+ info: clicksContext?.value.calculate(at),
52
51
  })
53
52
  delete props[k]
54
53
  }
@@ -59,11 +58,6 @@ export function createVMotionDirectives() {
59
58
  original.created!(el, binding, node, prevNode)
60
59
  original.mounted!(el, binding, node, prevNode)
61
60
 
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
61
  // @ts-expect-error extra prop
68
62
  const motion = el.motionInstance
69
63
  motion.clickIds = clicks.map(i => i.id)
@@ -71,7 +65,7 @@ export function createVMotionDirectives() {
71
65
  motion.watchStopHandle = watch(
72
66
  [thisPage, currentPage, currentClicks].filter(Boolean),
73
67
  () => {
74
- const visibility = clickVisibility?.value ?? true
68
+ const visibility = resolvedClickMap.get(el)?.visibilityState.value ?? 'shown'
75
69
  if (!clicksContext?.value || !['slide', 'presenter'].includes(renderContext?.value ?? '')) {
76
70
  const mixedVariant: Record<string, unknown> = { ...variantInitial, ...variantEnter }
77
71
  for (const { variant } of clicks)
@@ -80,10 +74,10 @@ export function createVMotionDirectives() {
80
74
  motion.set(mixedVariant)
81
75
  }
82
76
  else if (isPrintMode.value || thisPage?.value === currentPage.value) {
83
- if (visibility === true) {
77
+ if (visibility === 'shown') {
84
78
  const mixedVariant: Record<string, unknown> = { ...variantInitial, ...variantEnter }
85
- for (const { variant, resolved: resolvedClick } of clicks) {
86
- if (!resolvedClick || resolvedClick.isActive.value)
79
+ for (const { variant, info } of clicks) {
80
+ if (!info || info.isActive.value)
87
81
  Object.assign(mixedVariant, variant)
88
82
  }
89
83
  if (isPrintMode.value)
@@ -104,14 +98,9 @@ export function createVMotionDirectives() {
104
98
  },
105
99
  )
106
100
  },
107
- unmounted(el, dir) {
108
- if (!directiveInject(dir, injectionClicksContext)?.value)
109
- return
110
-
111
- const ctx = directiveInject(dir, injectionClicksContext)?.value
101
+ unmounted(el) {
112
102
  // @ts-expect-error extra prop
113
103
  const motion = el.motionInstance
114
- motion.clickIds.map((id: string) => ctx?.unregister(id))
115
104
  motion.watchStopHandle()
116
105
  },
117
106
  })
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@slidev/client",
3
3
  "type": "module",
4
- "version": "0.49.0-beta.1",
4
+ "version": "0.49.0-beta.2",
5
5
  "description": "Presentation slides for developers",
6
6
  "author": "antfu <anthonyfu117@hotmail.com>",
7
7
  "license": "MIT",
@@ -36,8 +36,8 @@
36
36
  "@shikijs/vitepress-twoslash": "^1.3.0",
37
37
  "@slidev/rough-notation": "^0.1.0",
38
38
  "@typescript/ata": "^0.9.4",
39
- "@unhead/vue": "^1.9.4",
40
- "@unocss/reset": "^0.59.0",
39
+ "@unhead/vue": "^1.9.5",
40
+ "@unocss/reset": "^0.59.2",
41
41
  "@vueuse/core": "^10.9.0",
42
42
  "@vueuse/math": "^10.9.0",
43
43
  "@vueuse/motion": "^2.1.0",
@@ -46,7 +46,6 @@
46
46
  "file-saver": "^2.0.5",
47
47
  "floating-vue": "^5.2.2",
48
48
  "fuse.js": "^7.0.0",
49
- "js-yaml": "^4.1.0",
50
49
  "katex": "^0.16.10",
51
50
  "lz-string": "^1.5.0",
52
51
  "mermaid": "^10.9.0",
@@ -55,13 +54,14 @@
55
54
  "recordrtc": "^5.6.2",
56
55
  "shiki": "^1.3.0",
57
56
  "shiki-magic-move": "^0.3.5",
58
- "typescript": "^5.4.4",
59
- "unocss": "^0.59.0",
57
+ "typescript": "^5.4.5",
58
+ "unocss": "^0.59.2",
60
59
  "vue": "^3.4.21",
61
60
  "vue-demi": "^0.14.7",
62
61
  "vue-router": "^4.3.0",
63
- "@slidev/parser": "0.49.0-beta.1",
64
- "@slidev/types": "0.49.0-beta.1"
62
+ "yaml": "^2.4.1",
63
+ "@slidev/parser": "0.49.0-beta.2",
64
+ "@slidev/types": "0.49.0-beta.2"
65
65
  },
66
66
  "devDependencies": {
67
67
  "vite": "^5.2.8"
package/pages/play.vue CHANGED
@@ -9,6 +9,7 @@ import SlideContainer from '../internals/SlideContainer.vue'
9
9
  import NavControls from '../internals/NavControls.vue'
10
10
  import SlidesShow from '../internals/SlidesShow.vue'
11
11
  import PrintStyle from '../internals/PrintStyle.vue'
12
+ import { onContextMenu } from '../logic/contextMenu'
12
13
  import { useNav } from '../composables/useNav'
13
14
  import { useDrawings } from '../composables/useDrawings'
14
15
 
@@ -22,9 +23,9 @@ function onClick(e: MouseEvent) {
22
23
  if (showEditor.value)
23
24
  return
24
25
 
25
- if ((e.target as HTMLElement)?.id === 'slide-container') {
26
+ if (e.button === 0 && (e.target as HTMLElement)?.id === 'slide-container') {
26
27
  // click right to next, left to previous
27
- if ((e.screenX / window.innerWidth) > 0.6)
28
+ if ((e.pageX / window.innerWidth) > 0.6)
28
29
  next()
29
30
  else
30
31
  prev()
@@ -57,6 +58,7 @@ if (__SLIDEV_FEATURE_DRAWINGS__)
57
58
  :scale="slideScale"
58
59
  :is-main="true"
59
60
  @pointerdown="onClick"
61
+ @contextmenu="onContextMenu"
60
62
  >
61
63
  <template #default>
62
64
  <SlidesShow render-context="slide" />
@@ -7,6 +7,7 @@ import { decreasePresenterFontSize, increasePresenterFontSize, presenterLayout,
7
7
  import { configs } from '../env'
8
8
  import { sharedState } from '../state/shared'
9
9
  import { registerShortcuts } from '../logic/shortcuts'
10
+ import { onContextMenu } from '../logic/contextMenu'
10
11
  import { getSlideClass } from '../utils'
11
12
  import { useTimer } from '../logic/utils'
12
13
  import { createFixedClicks } from '../composables/useClicks'
@@ -21,6 +22,7 @@ import SlidesShow from '../internals/SlidesShow.vue'
21
22
  import DrawingControls from '../internals/DrawingControls.vue'
22
23
  import IconButton from '../internals/IconButton.vue'
23
24
  import ClicksSlider from '../internals/ClicksSlider.vue'
25
+ import ContextMenu from '../internals/ContextMenu.vue'
24
26
  import { useNav } from '../composables/useNav'
25
27
  import { useDrawings } from '../composables/useDrawings'
26
28
 
@@ -112,6 +114,7 @@ onMounted(() => {
112
114
  <SlideContainer
113
115
  key="main"
114
116
  class="h-full w-full p-2 lg:p-4 flex-auto"
117
+ @contextmenu="onContextMenu"
115
118
  >
116
119
  <template #default>
117
120
  <SlidesShow render-context="presenter" />
@@ -209,6 +212,7 @@ onMounted(() => {
209
212
  </div>
210
213
  <Goto />
211
214
  <QuickOverview v-model="showOverview" />
215
+ <ContextMenu />
212
216
  </template>
213
217
 
214
218
  <style scoped>
@@ -26,9 +26,9 @@ export default createSingletonPromise(async () => {
26
26
  })
27
27
 
28
28
  const resolveId = async (specifier: string) => {
29
- if (!/^(@[^\/:]+?\/)?[^\/:]+$/.test(specifier))
30
- return specifier
31
- const res = await fetch(`/@slidev/resolve-id/${specifier}`)
29
+ if (!'./'.includes(specifier[0]) && !/^(@[^\/:]+?\/)?[^\/:]+$/.test(specifier))
30
+ return specifier // this might be a url or something else
31
+ const res = await fetch(`/@slidev/resolve-id?specifier=${specifier}`)
32
32
  if (!res.ok)
33
33
  return null
34
34
  const id = await res.text()
@@ -85,7 +85,10 @@ async function runJavaScript(code: string): Promise<CodeRunnerOutputs> {
85
85
  replace.clear = () => allLogs.length = 0
86
86
  const vmConsole = Object.assign({}, console, replace)
87
87
  try {
88
- const safeJS = `return async (console) => {${sanitizeJS(code)}}`
88
+ const safeJS = `return async (console) => {
89
+ window.console = console
90
+ ${sanitizeJS(code)}
91
+ }`
89
92
  // eslint-disable-next-line no-new-func
90
93
  await (new Function(safeJS)())(vmConsole)
91
94
  }
@@ -0,0 +1,113 @@
1
+ /// <reference types="unplugin-icons/types/vue3" />
2
+ import type { ComputedRef } from 'vue'
3
+ import { computed } from 'vue'
4
+ import type { ContextMenuItem } from '@slidev/types'
5
+ import { useNav } from '../composables/useNav'
6
+ import { useDrawings } from '../composables/useDrawings'
7
+ import { fullscreen, showEditor, toggleOverview } from '../state'
8
+ import setups from '#slidev/setups/context-menu'
9
+
10
+ import IconArrowLeft from '~icons/carbon/arrow-left'
11
+ import IconArrowRight from '~icons/carbon/arrow-right'
12
+ import IconArrowUp from '~icons/carbon/arrow-up'
13
+ import IconArrowDown from '~icons/carbon/arrow-down'
14
+ import IconPen from '~icons/carbon/pen'
15
+ import IconTextNotationToggle from '~icons/carbon/text-annotation-toggle'
16
+ import IconApps from '~icons/carbon/apps'
17
+ import IconPresentationFile from '~icons/carbon/presentation-file'
18
+ import IconUserSpeaker from '~icons/carbon/user-speaker'
19
+ import IconMaximize from '~icons/carbon/maximize'
20
+ import IconMinimize from '~icons/carbon/minimize'
21
+
22
+ let items: ComputedRef<ContextMenuItem[]> | undefined
23
+
24
+ export default () => {
25
+ if (items)
26
+ return items
27
+
28
+ const {
29
+ next,
30
+ nextSlide,
31
+ prev,
32
+ prevSlide,
33
+ hasNext,
34
+ hasPrev,
35
+ currentPage,
36
+ total,
37
+ isPresenter,
38
+ enterPresenter,
39
+ exitPresenter,
40
+ isEmbedded,
41
+ isPresenterAvailable,
42
+ } = useNav()
43
+ const { drawingEnabled } = useDrawings()
44
+ const {
45
+ isFullscreen,
46
+ toggle: toggleFullscreen,
47
+ } = fullscreen
48
+
49
+ return items = setups.reduce(
50
+ (items, fn) => fn(items),
51
+ computed(() => [
52
+ {
53
+ small: true,
54
+ icon: IconArrowLeft,
55
+ label: 'Previous Click',
56
+ action: prev,
57
+ disabled: !hasPrev.value,
58
+ },
59
+ {
60
+ small: true,
61
+ icon: IconArrowRight,
62
+ label: 'Next Click',
63
+ action: next,
64
+ disabled: !hasNext.value,
65
+ },
66
+ {
67
+ small: true,
68
+ icon: IconArrowUp,
69
+ label: 'Previous Slide',
70
+ action: prevSlide,
71
+ disabled: currentPage.value <= 1,
72
+ },
73
+ {
74
+ small: true,
75
+ icon: IconArrowDown,
76
+ label: 'Next Slide',
77
+ action: nextSlide,
78
+ disabled: currentPage.value >= total.value,
79
+ },
80
+ 'separator',
81
+ {
82
+ icon: IconTextNotationToggle,
83
+ label: showEditor.value ? 'Hide editor' : 'Show editor',
84
+ action: () => (showEditor.value = !showEditor.value),
85
+ },
86
+ {
87
+ icon: IconPen,
88
+ label: drawingEnabled.value ? 'Hide drawing toolbar' : 'Show drawing toolbar',
89
+ action: () => (drawingEnabled.value = !drawingEnabled.value),
90
+ },
91
+ {
92
+ icon: IconApps,
93
+ label: 'Show slide overview',
94
+ action: toggleOverview,
95
+ },
96
+ isPresenter.value && {
97
+ icon: IconPresentationFile,
98
+ label: 'Exit Presenter Mode',
99
+ action: exitPresenter,
100
+ },
101
+ __SLIDEV_FEATURE_PRESENTER__ && isPresenterAvailable.value && {
102
+ icon: IconUserSpeaker,
103
+ label: 'Enter Presenter Mode',
104
+ action: enterPresenter,
105
+ },
106
+ !isEmbedded.value && {
107
+ icon: isFullscreen.value ? IconMinimize : IconMaximize,
108
+ label: isFullscreen.value ? 'Close fullscreen' : 'Enter fullscreen',
109
+ action: toggleFullscreen,
110
+ },
111
+ ].filter(Boolean) as ContextMenuItem[]),
112
+ )
113
+ }