@slidev/client 0.48.9 → 0.49.0-beta.1

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.
@@ -1,86 +1,78 @@
1
1
  <script setup lang="ts">
2
- import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
2
+ import { computed, onMounted, ref, watch } from 'vue'
3
+ import { and } from '@vueuse/math'
3
4
  import { useSlideContext } from '../context'
5
+ import { useNav } from '../composables/useNav'
4
6
 
5
7
  const props = defineProps<{
6
- autoPlay?: boolean | 'once' | 'resume' | 'resumeOnce'
7
- autoPause?: 'slide' | 'click'
8
- autoReset?: 'slide' | 'click'
8
+ autoplay?: boolean | 'once'
9
+ autoreset?: 'slide' | 'click'
10
+ poster?: string
11
+ printPoster?: string
12
+ timestamp?: string | number
13
+ printTimestamp?: string | number | 'last'
14
+ controls?: boolean
9
15
  }>()
10
16
 
17
+ const printPoster = computed(() => props.printPoster ?? props.poster)
18
+ const printTimestamp = computed(() => props.printTimestamp ?? props.timestamp ?? 0)
19
+
11
20
  const {
12
21
  $slidev,
13
- $clicksContext: clicks,
14
- $renderContext: currentContext,
15
- $route: route,
22
+ $clicksContext,
23
+ $renderContext,
24
+ $route,
16
25
  } = useSlideContext()
26
+ const { isPrintMode } = useNav()
27
+
28
+ const noPlay = computed(() => isPrintMode.value || !['slide', 'presenter'].includes($renderContext.value))
17
29
 
18
30
  const video = ref<HTMLMediaElement>()
19
31
  const played = ref(false)
20
- const ended = ref(false)
21
-
22
- const matchRoute = computed(() => {
23
- if (!video.value || currentContext?.value !== 'slide')
24
- return false
25
- return route && route.no === $slidev?.nav.currentSlideNo
26
- })
27
-
28
- const matchClick = computed(() => {
29
- if (!video.value || currentContext?.value !== 'slide' || !clicks)
30
- return false
31
- return clicks.map.get(video.value)?.isShown?.value ?? true
32
- })
33
-
34
- const matchRouteAndClick = computed(() => matchRoute.value && matchClick.value)
35
-
36
- watch(matchRouteAndClick, () => {
37
- if (!video.value || currentContext?.value !== 'slide')
38
- return
39
-
40
- if (matchRouteAndClick.value) {
41
- if (props.autoReset === 'click')
42
- video.value.currentTime = 0
43
- if (props.autoPlay && (!played.value || props.autoPlay === 'resume' || (props.autoPlay === 'resumeOnce' && !ended.value)))
44
- video.value.play()
45
- }
46
-
47
- if ((props.autoPause === 'click' && !matchRouteAndClick.value) || (props.autoPause === 'slide' && !matchRoute.value))
48
- video.value.pause()
49
- })
50
32
 
51
- watch(matchRoute, () => {
52
- if (!video.value || currentContext?.value !== 'slide')
33
+ onMounted(() => {
34
+ if (noPlay.value)
53
35
  return
54
36
 
55
- if (matchRoute.value && props.autoReset === 'slide')
56
- video.value.currentTime = 0
37
+ const timestamp = +(props.timestamp ?? 0)
38
+ video.value!.currentTime = timestamp
39
+
40
+ const matchRoute = computed(() => !!$route && $route.no === $slidev?.nav.currentSlideNo)
41
+ const matchClick = computed(() => !!video.value && ($clicksContext.map.get(video.value)?.isShown?.value ?? true))
42
+ const matchRouteAndClick = and(matchRoute, matchClick)
43
+
44
+ watch(matchRouteAndClick, () => {
45
+ if (matchRouteAndClick.value) {
46
+ if (props.autoplay === true || (props.autoplay === 'once' && !played.value))
47
+ video.value!.play()
48
+ }
49
+ else {
50
+ video.value!.pause()
51
+ if (props.autoreset === 'click' || (props.autoreset === 'slide' && !matchRoute.value))
52
+ video.value!.currentTime = timestamp
53
+ }
54
+ }, { immediate: true })
57
55
  })
58
56
 
59
- function onPlay() {
60
- played.value = true
61
- }
62
-
63
- function onEnded() {
64
- ended.value = true
57
+ function onLoadedMetadata(ev: Event) {
58
+ // The video may be loaded before component mounted
59
+ const element = ev.target as HTMLMediaElement
60
+ if (noPlay.value && (!printPoster.value || props.printTimestamp)) {
61
+ element.currentTime = printTimestamp.value === 'last'
62
+ ? element.duration
63
+ : +printTimestamp.value
64
+ }
65
65
  }
66
-
67
- onMounted(() => {
68
- if (!video.value || currentContext?.value !== 'slide')
69
- return
70
- video.value?.addEventListener('play', onPlay)
71
- video.value?.addEventListener('ended', onEnded)
72
- })
73
-
74
- onUnmounted(() => {
75
- if (!video.value || currentContext?.value !== 'slide')
76
- return
77
- video.value?.removeEventListener('play', onPlay)
78
- video.value?.removeEventListener('ended', onEnded)
79
- })
80
66
  </script>
81
67
 
82
68
  <template>
83
- <video ref="video">
69
+ <video
70
+ ref="video"
71
+ :poster="noPlay ? printPoster : props.poster"
72
+ :controls="!noPlay && props.controls"
73
+ @play="played = true"
74
+ @loadedmetadata="onLoadedMetadata"
75
+ >
84
76
  <slot />
85
77
  </video>
86
78
  </template>
@@ -0,0 +1,27 @@
1
+ <script setup lang="ts">
2
+ import { onMounted, onUnmounted } from 'vue'
3
+ import type { DragElementMarkdownSource } from '../composables/useDragElements'
4
+ import { useDragElement } from '../composables/useDragElements'
5
+
6
+ const props = defineProps<{
7
+ pos?: string
8
+ markdownSource?: DragElementMarkdownSource
9
+ }>()
10
+
11
+ const { id, container, containerStyle, mounted, unmounted, startDragging } = useDragElement(null, props.pos, props.markdownSource)
12
+
13
+ onMounted(mounted)
14
+ onUnmounted(unmounted)
15
+ </script>
16
+
17
+ <template>
18
+ <div
19
+ ref="container"
20
+ :data-drag-id="id"
21
+ :style="containerStyle"
22
+ class="p-1"
23
+ @dblclick="startDragging"
24
+ >
25
+ <slot />
26
+ </div>
27
+ </template>
@@ -0,0 +1,282 @@
1
+ import { debounce, ensureSuffix } from '@antfu/utils'
2
+ import type { SlidePatch } from '@slidev/types'
3
+ import { injectLocal, onClickOutside, useWindowFocus } from '@vueuse/core'
4
+ import type { CSSProperties, DirectiveBinding, InjectionKey, WatchStopHandle } from 'vue'
5
+ import { computed, ref, watch } from 'vue'
6
+ import { injectionCurrentPage, injectionFrontmatter, injectionRenderContext, injectionSlideElement, injectionSlideScale, injectionSlideZoom } from '../constants'
7
+ import { makeId } from '../logic/utils'
8
+ import { activeDragElement } from '../state'
9
+ import { directiveInject } from '../utils'
10
+ import { useSlideBounds } from './useSlideBounds'
11
+ import { useDynamicSlideInfo } from './useSlideInfo'
12
+
13
+ export type DragElementDataSource = 'frontmatter' | 'prop' | 'directive'
14
+ /**
15
+ * Markdown source position, injected by markdown-it plugin
16
+ */
17
+ export type DragElementMarkdownSource = [startLine: number, endLine: number, index: number]
18
+
19
+ export type DragElementsUpdater = (id: string, posStr: string, type: DragElementDataSource, markdownSource?: DragElementMarkdownSource) => void
20
+
21
+ const map: Record<number, DragElementsUpdater> = {}
22
+
23
+ export function useDragElementsUpdater(no: number) {
24
+ if (!(__DEV__ && __SLIDEV_FEATURE_EDITOR__))
25
+ return () => {}
26
+
27
+ if (map[no])
28
+ return map[no]
29
+
30
+ const { info, update } = useDynamicSlideInfo(no)
31
+
32
+ let newPatch: SlidePatch | null = null
33
+ async function save() {
34
+ if (newPatch) {
35
+ await update({
36
+ ...newPatch,
37
+ skipHmr: true,
38
+ })
39
+ newPatch = null
40
+ }
41
+ }
42
+ const debouncedSave = debounce(500, save)
43
+
44
+ return map[no] = (id, posStr, type, markdownSource) => {
45
+ if (!info.value)
46
+ return
47
+
48
+ if (type === 'frontmatter') {
49
+ const frontmatter = info.value.frontmatter
50
+ frontmatter.dragPos ||= {}
51
+ if (frontmatter.dragPos[id] === posStr)
52
+ return
53
+ frontmatter.dragPos[id] = posStr
54
+ newPatch = {
55
+ frontmatter,
56
+ }
57
+ }
58
+ else {
59
+ if (!markdownSource)
60
+ throw new Error(`[Slidev] VDrag Element ${id} is missing markdown source`)
61
+
62
+ const [startLine, endLine, idx] = markdownSource
63
+ const lines = info.value.content.split(/\r?\n/g)
64
+
65
+ let section = lines.slice(startLine, endLine).join('\n')
66
+ let replaced = false
67
+
68
+ section = type === 'prop'
69
+ ? section.replace(/<(v-?drag)(.*?)>/ig, (full, tag, attrs, index) => {
70
+ if (index === idx) {
71
+ replaced = true
72
+ const posMatch = attrs.match(/pos=".*?"/)
73
+ if (!posMatch)
74
+ return `<${tag}${ensureSuffix(' ', attrs)}pos="${posStr}">`
75
+ const start = posMatch.index
76
+ const end = start + posMatch[0].length
77
+ return `<${tag}${attrs.slice(0, start)}pos="${posStr}"${attrs.slice(end)}>`
78
+ }
79
+ return full
80
+ })
81
+ : section.replace(/(?<![</\w])v-drag(?:=".*?")?/ig, (full, index) => {
82
+ if (index === idx) {
83
+ replaced = true
84
+ return `v-drag="${posStr}"`
85
+ }
86
+ return full
87
+ })
88
+
89
+ if (!replaced)
90
+ throw new Error(`[Slidev] VDrag Element ${id} is not found in the markdown source`)
91
+
92
+ lines.splice(
93
+ startLine,
94
+ endLine - startLine,
95
+ section,
96
+ )
97
+
98
+ const newContent = lines.join('\n')
99
+ if (info.value.content === newContent)
100
+ return
101
+ newPatch = {
102
+ content: newContent,
103
+ }
104
+ info.value = {
105
+ ...info.value,
106
+ content: newContent,
107
+ }
108
+ }
109
+ debouncedSave()
110
+ }
111
+ }
112
+
113
+ export function useDragElement(directive: DirectiveBinding | null, posRaw?: string | number | number[], markdownSource?: DragElementMarkdownSource) {
114
+ function inject<T>(key: InjectionKey<T> | string): T | undefined {
115
+ return directive
116
+ ? directiveInject(directive, key)
117
+ : injectLocal(key)
118
+ }
119
+
120
+ const renderContext = inject(injectionRenderContext)!
121
+ const frontmatter = inject(injectionFrontmatter) ?? {}
122
+ const page = inject(injectionCurrentPage)!
123
+ const updater = computed(() => useDragElementsUpdater(page.value))
124
+ const scale = inject(injectionSlideScale) ?? ref(1)
125
+ const zoom = inject(injectionSlideZoom) ?? ref(1)
126
+ const { left: slideLeft, top: slideTop, stop: stopWatchBounds } = useSlideBounds(inject(injectionSlideElement) ?? ref())
127
+ const enabled = ['slide', 'presenter'].includes(renderContext.value)
128
+
129
+ let dataSource: DragElementDataSource = directive ? 'directive' : 'prop'
130
+ let id: string = makeId()
131
+ let pos: number[] | undefined
132
+ if (Array.isArray(posRaw)) {
133
+ pos = posRaw
134
+ }
135
+ else if (typeof posRaw === 'string' && posRaw.includes(',')) {
136
+ pos = posRaw.split(',').map(Number)
137
+ }
138
+ else if (posRaw != null) {
139
+ dataSource = 'frontmatter'
140
+ id = `${posRaw}`
141
+ posRaw = frontmatter?.dragPos?.[id]
142
+ pos = (posRaw as string)?.split(',').map(Number)
143
+ }
144
+
145
+ if (dataSource !== 'frontmatter' && !markdownSource)
146
+ throw new Error('[Slidev] Can not identify the source position of the v-drag element, please provide an explicit `id` prop.')
147
+
148
+ const watchStopHandles: WatchStopHandle[] = [stopWatchBounds]
149
+
150
+ const autoHeight = posRaw != null && !Number.isFinite(pos?.[3])
151
+ pos ??= [Number.NaN, Number.NaN, 0]
152
+ const width = ref(pos[2])
153
+ const x0 = ref(pos[0] + pos[2] / 2)
154
+
155
+ const rotate = ref(pos[4] ?? 0)
156
+ const rotateRad = computed(() => rotate.value * Math.PI / 180)
157
+ const rotateSin = computed(() => Math.sin(rotateRad.value))
158
+ const rotateCos = computed(() => Math.cos(rotateRad.value))
159
+
160
+ const container = ref<HTMLElement>()
161
+ const bounds = ref({ left: 0, top: 0, width: 0, height: 0 })
162
+ const actualHeight = ref(0)
163
+ function updateBounds() {
164
+ const rect = container.value!.getBoundingClientRect()
165
+ bounds.value = {
166
+ left: rect.left / zoom.value,
167
+ top: rect.top / zoom.value,
168
+ width: rect.width / zoom.value,
169
+ height: rect.height / zoom.value,
170
+ }
171
+ actualHeight.value = ((bounds.value.width + bounds.value.height) / scale.value / (Math.abs(rotateSin.value) + Math.abs(rotateCos.value)) - width.value)
172
+ }
173
+ watchStopHandles.push(watch(width, updateBounds, { flush: 'post' }))
174
+
175
+ const configuredHeight = ref(pos[3] ?? 0)
176
+ const height = computed({
177
+ get: () => (autoHeight ? actualHeight.value : configuredHeight.value) || 0,
178
+ set: v => !autoHeight && (configuredHeight.value = v),
179
+ })
180
+ const configuredY0 = ref(pos[1])
181
+ const y0 = computed({
182
+ get: () => configuredY0.value + height.value / 2,
183
+ set: v => configuredY0.value = v - height.value / 2,
184
+ })
185
+
186
+ const containerStyle = computed<CSSProperties>(() => {
187
+ return Number.isFinite(x0.value)
188
+ ? {
189
+ position: 'absolute',
190
+ zIndex: 100,
191
+ left: `${x0.value - width.value / 2}px`,
192
+ top: `${y0.value - height.value / 2}px`,
193
+ width: `${width.value}px`,
194
+ height: autoHeight ? undefined : `${height.value}px`,
195
+ transformOrigin: 'center center',
196
+ transform: `rotate(${rotate.value}deg)`,
197
+ }
198
+ : {
199
+ position: 'absolute',
200
+ zIndex: 100,
201
+ }
202
+ })
203
+
204
+ watchStopHandles.push(
205
+ watch(
206
+ [x0, y0, width, height, rotate],
207
+ ([x0, y0, w, h, r]) => {
208
+ let posStr = [x0 - w / 2, y0 - h / 2, w].map(Math.round).join()
209
+ if (autoHeight)
210
+ posStr += dataSource === 'directive' ? ',NaN' : ',_'
211
+ else
212
+ posStr += `,${Math.round(h)}`
213
+ if (Math.round(r) !== 0)
214
+ posStr += `,${Math.round(r)}`
215
+
216
+ if (dataSource === 'directive')
217
+ posStr = `[${posStr}]`
218
+
219
+ updater.value(id, posStr, dataSource, markdownSource)
220
+ },
221
+ ),
222
+ )
223
+
224
+ const state = {
225
+ id,
226
+ dataSource,
227
+ markdownSource,
228
+ zoom,
229
+ autoHeight,
230
+ x0,
231
+ y0,
232
+ width,
233
+ height,
234
+ rotate,
235
+ container,
236
+ containerStyle,
237
+ watchStopHandles,
238
+ dragging: computed((): boolean => activeDragElement.value === state),
239
+ mounted() {
240
+ if (!enabled)
241
+ return
242
+ updateBounds()
243
+ if (!posRaw) {
244
+ setTimeout(() => {
245
+ updateBounds()
246
+ x0.value = (bounds.value.left + bounds.value.width / 2 - slideLeft.value) / scale.value
247
+ y0.value = (bounds.value.top - slideTop.value) / scale.value
248
+ width.value = bounds.value.width / scale.value
249
+ height.value = bounds.value.height / scale.value
250
+ }, 100)
251
+ }
252
+ },
253
+ unmounted() {
254
+ if (!enabled)
255
+ return
256
+ state.stopDragging()
257
+ },
258
+ startDragging(): void {
259
+ updateBounds()
260
+ activeDragElement.value = state
261
+ },
262
+ stopDragging(): void {
263
+ if (activeDragElement.value === state)
264
+ activeDragElement.value = null
265
+ },
266
+ }
267
+
268
+ watchStopHandles.push(
269
+ onClickOutside(container, (ev) => {
270
+ if ((ev.target as HTMLElement | null)?.dataset?.dragId !== id)
271
+ state.stopDragging()
272
+ }),
273
+ watch(useWindowFocus(), (focused) => {
274
+ if (!focused)
275
+ state.stopDragging()
276
+ }),
277
+ )
278
+
279
+ return state
280
+ }
281
+
282
+ export type DragElementState = ReturnType<typeof useDragElement>
@@ -0,0 +1,30 @@
1
+ import { useElementBounding } from '@vueuse/core'
2
+ import { inject, ref, watch } from 'vue'
3
+ import { injectionSlideElement } from '../constants'
4
+ import { editorHeight, editorWidth, isEditorVertical, showEditor, slideScale, windowSize } from '../state'
5
+
6
+ export function useSlideBounds(slideElement = inject(injectionSlideElement, ref())) {
7
+ const bounding = useElementBounding(slideElement)
8
+ const stop = watch(
9
+ [
10
+ showEditor,
11
+ isEditorVertical,
12
+ editorWidth,
13
+ editorHeight,
14
+ slideScale,
15
+ windowSize.width,
16
+ windowSize.height,
17
+ ],
18
+ () => {
19
+ setTimeout(bounding.update, 300)
20
+ },
21
+ {
22
+ flush: 'post',
23
+ immediate: true,
24
+ },
25
+ )
26
+ return {
27
+ ...bounding,
28
+ stop,
29
+ }
30
+ }
@@ -6,19 +6,19 @@ import type { SlideInfo, SlidePatch } from '@slidev/types'
6
6
  import { getSlide } from '../logic/slides'
7
7
 
8
8
  export interface UseSlideInfo {
9
- info: Ref<SlideInfo | undefined>
9
+ info: Ref<SlideInfo | null>
10
10
  update: (data: SlidePatch) => Promise<SlideInfo | void>
11
11
  }
12
12
 
13
13
  export function useSlideInfo(no: number): UseSlideInfo {
14
14
  if (!__SLIDEV_HAS_SERVER__) {
15
15
  return {
16
- info: ref(getSlide(no)?.meta.slide) as Ref<SlideInfo | undefined>,
16
+ info: ref(getSlide(no)?.meta.slide ?? null) as Ref<SlideInfo | null>,
17
17
  update: async () => {},
18
18
  }
19
19
  }
20
20
  const url = `/@slidev/slide/${no}.json`
21
- const { data: info, execute } = useFetch(url).json().get()
21
+ const { data: info, execute } = useFetch(url).json<SlideInfo>().get()
22
22
 
23
23
  execute()
24
24
 
@@ -42,7 +42,7 @@ export function useSlideInfo(no: number): UseSlideInfo {
42
42
  info.value = payload.data
43
43
  })
44
44
  import.meta.hot?.on('slidev:update-note', (payload) => {
45
- if (payload.no === no && info.value.note?.trim() !== payload.note?.trim())
45
+ if (payload.no === no && info.value && info.value.note?.trim() !== payload.note?.trim())
46
46
  info.value = { ...info.value, ...payload }
47
47
  })
48
48
  }
@@ -61,7 +61,14 @@ export function useDynamicSlideInfo(no: MaybeRef<number>) {
61
61
  }
62
62
 
63
63
  return {
64
- info: computed(() => get(unref(no)).info.value),
64
+ info: computed({
65
+ get() {
66
+ return get(unref(no)).info.value
67
+ },
68
+ set(newInfo) {
69
+ get(unref(no)).info.value = newInfo
70
+ },
71
+ }),
65
72
  update: async (data: SlidePatch, newId?: number) => {
66
73
  const info = get(newId ?? unref(no))
67
74
  const newData = await info.update(data)
package/constants.ts CHANGED
@@ -6,6 +6,7 @@ import type { SlidevContext } from './modules/context'
6
6
  // The value of the injections keys are implementation details, you should always use them with the reference to the constant instead of the value
7
7
  export const injectionClicksContext = '$$slidev-clicks-context' as unknown as InjectionKey<Ref<ClicksContext>>
8
8
  export const injectionCurrentPage = '$$slidev-page' as unknown as InjectionKey<Ref<number>>
9
+ export const injectionSlideElement = '$$slidev-slide-element' as unknown as InjectionKey<Ref<HTMLElement | null>>
9
10
  export const injectionSlideScale = '$$slidev-slide-scale' as unknown as InjectionKey<ComputedRef<number>>
10
11
  export const injectionSlidevContext = '$$slidev-context' as unknown as InjectionKey<UnwrapNestedRefs<SlidevContext>>
11
12
  export const injectionRoute = '$$slidev-route' as unknown as InjectionKey<SlideRoute>
@@ -44,6 +45,7 @@ export const FRONTMATTER_FIELDS = [
44
45
  'title',
45
46
  'transition',
46
47
  'zoom',
48
+ 'dragPos',
47
49
  ]
48
50
 
49
51
  export const HEADMATTER_FIELDS = [
@@ -0,0 +1,396 @@
1
+ <script setup lang="ts">
2
+ import { clamp } from '@antfu/utils'
3
+ import type { Pausable } from '@vueuse/core'
4
+ import { useIntervalFn } from '@vueuse/core'
5
+ import { computed, inject, ref, watchEffect } from 'vue'
6
+ import type { DragElementState } from '../composables/useDragElements'
7
+ import { useSlideBounds } from '../composables/useSlideBounds'
8
+ import { injectionSlideScale } from '../constants'
9
+ import { slideHeight, slideWidth } from '../env'
10
+ import { magicKeys } from '../state'
11
+
12
+ const { data } = defineProps<{ data: DragElementState }>()
13
+ const { id, zoom, autoHeight, x0, y0, width, height, rotate } = data
14
+
15
+ const slideScale = inject(injectionSlideScale, ref(1))
16
+ const scale = computed(() => slideScale.value * zoom.value)
17
+ const { left: slideLeft, top: slideTop } = useSlideBounds()
18
+
19
+ const ctrlSize = 10
20
+ const minSize = 40
21
+ const minRemain = 10
22
+
23
+ const rotateRad = computed(() => rotate.value * Math.PI / 180)
24
+ const rotateSin = computed(() => Math.sin(rotateRad.value))
25
+ const rotateCos = computed(() => Math.cos(rotateRad.value))
26
+
27
+ const boundingWidth = computed(() => width.value * rotateCos.value + height.value * rotateSin.value)
28
+ const boundingHeight = computed(() => width.value * rotateSin.value + height.value * rotateCos.value)
29
+
30
+ const boundingLeft = computed(() => x0.value - boundingWidth.value / 2)
31
+ const boundingTop = computed(() => y0.value - boundingHeight.value / 2)
32
+ const boundingRight = computed(() => x0.value + boundingWidth.value / 2)
33
+ const boundingBottom = computed(() => y0.value + boundingHeight.value / 2)
34
+
35
+ let currentDrag: {
36
+ x0: number
37
+ y0: number
38
+ width: number
39
+ height: number
40
+ rotate: number
41
+ dx0: number
42
+ dy0: number
43
+ ltx: number
44
+ lty: number
45
+ rtx: number
46
+ rty: number
47
+ lbx: number
48
+ lby: number
49
+ rbx: number
50
+ rby: number
51
+ } | null = null
52
+
53
+ function onPointerdown(ev: PointerEvent) {
54
+ if (ev.buttons !== 1)
55
+ return
56
+
57
+ ev.preventDefault()
58
+ ev.stopPropagation()
59
+ const el = ev.target as HTMLElement
60
+ const elBounds = el.getBoundingClientRect()
61
+
62
+ const cross1x = width.value * rotateCos.value - height.value * rotateSin.value
63
+ const cross1y = width.value * rotateSin.value + height.value * rotateCos.value
64
+ const cross2x = width.value * rotateCos.value + height.value * rotateSin.value
65
+ const cross2y = -width.value * rotateSin.value + height.value * rotateCos.value
66
+
67
+ currentDrag = {
68
+ x0: x0.value,
69
+ y0: y0.value,
70
+ width: width.value,
71
+ height: height.value,
72
+ rotate: rotate.value,
73
+ dx0: ev.clientX - (elBounds.left + elBounds.right) / 2,
74
+ dy0: ev.clientY - (elBounds.top + elBounds.bottom) / 2,
75
+ ltx: x0.value - cross1x / 2,
76
+ lty: y0.value - cross1y / 2,
77
+ rtx: x0.value + cross2x / 2,
78
+ rty: y0.value - cross2y / 2,
79
+ lbx: x0.value - cross2x / 2,
80
+ lby: y0.value + cross2y / 2,
81
+ rbx: x0.value + cross1x / 2,
82
+ rby: y0.value + cross1y / 2,
83
+ };
84
+
85
+ (ev.currentTarget as HTMLElement).setPointerCapture(ev.pointerId)
86
+ }
87
+
88
+ function onPointermove(ev: PointerEvent) {
89
+ if (!currentDrag || ev.buttons !== 1)
90
+ return
91
+
92
+ ev.preventDefault()
93
+ ev.stopPropagation()
94
+
95
+ const x = (ev.clientX - slideLeft.value - currentDrag.dx0) / scale.value
96
+ const y = (ev.clientY - slideTop.value - currentDrag.dy0) / scale.value
97
+
98
+ x0.value = clamp(x, -boundingWidth.value / 2 + minRemain, slideWidth.value + boundingWidth.value / 2 - minRemain)
99
+ y0.value = clamp(y, -boundingHeight.value / 2 + minRemain, slideHeight.value + boundingHeight.value / 2 - minRemain)
100
+ }
101
+
102
+ function onPointerup(ev: PointerEvent) {
103
+ if (!currentDrag)
104
+ return
105
+
106
+ ev.preventDefault()
107
+ ev.stopPropagation()
108
+
109
+ currentDrag = null
110
+ }
111
+
112
+ const ctrlClasses = `absolute border border-gray bg-gray dark:border-gray-500 dark:bg-gray-800 bg-opacity-30 `
113
+
114
+ function getCornerProps(isLeft: boolean, isTop: boolean) {
115
+ return {
116
+ onPointerdown,
117
+ onPointermove: (ev: PointerEvent) => {
118
+ if (!currentDrag || ev.buttons !== 1)
119
+ return
120
+
121
+ ev.preventDefault()
122
+ ev.stopPropagation()
123
+
124
+ let x = (ev.clientX - slideLeft.value) / scale.value
125
+ let y = (ev.clientY - slideTop.value) / scale.value
126
+
127
+ const { ltx, lty, rtx, rty, lbx, lby, rbx, rby } = currentDrag
128
+
129
+ const ratio = currentDrag.width / currentDrag.height
130
+ const wMin = Math.max(minSize, minSize * ratio)
131
+ function getSize(w1: number, h1: number) {
132
+ if (ev.shiftKey) {
133
+ const w = Math.max(w1, h1 * ratio, wMin)
134
+ const h = w / ratio
135
+ return { w, h }
136
+ }
137
+ else {
138
+ return { w: Math.max(w1, minSize), h: Math.max(h1, minSize) }
139
+ }
140
+ }
141
+
142
+ if (isLeft) {
143
+ if (isTop) {
144
+ const w1 = (rbx - x) * rotateCos.value + (rby - y) * rotateSin.value
145
+ const h1 = -(rbx - x) * rotateSin.value + (rby - y) * rotateCos.value
146
+ const { w, h } = getSize(w1, h1)
147
+ x = rbx - w * rotateCos.value + h * rotateSin.value
148
+ y = rby - w * rotateSin.value - h * rotateCos.value
149
+ }
150
+ else {
151
+ const w1 = (rtx - x) * rotateCos.value - (y - rty) * rotateSin.value
152
+ const h1 = (rtx - x) * rotateSin.value + (y - rty) * rotateCos.value
153
+ const { w, h } = getSize(w1, h1)
154
+ x = rtx - w * rotateCos.value - h * rotateSin.value
155
+ y = rty - w * rotateSin.value + h * rotateCos.value
156
+ }
157
+ }
158
+ else {
159
+ if (isTop) {
160
+ const w1 = (x - lbx) * rotateCos.value - (lby - y) * rotateSin.value
161
+ const h1 = (x - lbx) * rotateSin.value + (lby - y) * rotateCos.value
162
+ const { w, h } = getSize(w1, h1)
163
+ x = lbx + w * rotateCos.value + h * rotateSin.value
164
+ y = lby + w * rotateSin.value - h * rotateCos.value
165
+ }
166
+ else {
167
+ const w1 = (x - ltx) * rotateCos.value + (y - lty) * rotateSin.value
168
+ const h1 = -(x - ltx) * rotateSin.value + (y - lty) * rotateCos.value
169
+ const { w, h } = getSize(w1, h1)
170
+ x = ltx + w * rotateCos.value - h * rotateSin.value
171
+ y = lty + w * rotateSin.value + h * rotateCos.value
172
+ }
173
+ }
174
+
175
+ if (isLeft) {
176
+ if (isTop) {
177
+ x0.value = (x + rbx) / 2
178
+ y0.value = (y + rby) / 2
179
+ width.value = (rbx - x) * rotateCos.value + (rby - y) * rotateSin.value
180
+ height.value = -(rbx - x) * rotateSin.value + (rby - y) * rotateCos.value
181
+ }
182
+ else {
183
+ x0.value = (x + rtx) / 2
184
+ y0.value = (y + rty) / 2
185
+ width.value = (rtx - x) * rotateCos.value - (y - rty) * rotateSin.value
186
+ height.value = (rtx - x) * rotateSin.value + (y - rty) * rotateCos.value
187
+ }
188
+ }
189
+ else {
190
+ if (isTop) {
191
+ x0.value = (x + lbx) / 2
192
+ y0.value = (y + lby) / 2
193
+ width.value = (x - lbx) * rotateCos.value - (lby - y) * rotateSin.value
194
+ height.value = (x - lbx) * rotateSin.value + (lby - y) * rotateCos.value
195
+ }
196
+ else {
197
+ x0.value = (x + ltx) / 2
198
+ y0.value = (y + lty) / 2
199
+ width.value = (x - ltx) * rotateCos.value + (y - lty) * rotateSin.value
200
+ height.value = -(x - ltx) * rotateSin.value + (y - lty) * rotateCos.value
201
+ }
202
+ }
203
+ },
204
+ onPointerup,
205
+ style: {
206
+ width: `${ctrlSize}px`,
207
+ height: `${ctrlSize}px`,
208
+ margin: `-${ctrlSize / 2}px`,
209
+ left: isLeft ? '0' : undefined,
210
+ right: isLeft ? undefined : '0',
211
+ top: isTop ? '0' : undefined,
212
+ bottom: isTop ? undefined : '0',
213
+ cursor: +isLeft + +isTop === 1 ? 'nesw-resize' : 'nwse-resize',
214
+ },
215
+ class: ctrlClasses,
216
+ }
217
+ }
218
+
219
+ function getBorderProps(dir: 'l' | 'r' | 't' | 'b') {
220
+ return {
221
+ onPointerdown,
222
+ onPointermove: (ev: PointerEvent) => {
223
+ if (!currentDrag || ev.buttons !== 1)
224
+ return
225
+
226
+ ev.preventDefault()
227
+ ev.stopPropagation()
228
+
229
+ const x = (ev.clientX - slideLeft.value) / scale.value
230
+ const y = (ev.clientY - slideTop.value) / scale.value
231
+
232
+ const { ltx, lty, rtx, rty, lbx, lby, rbx, rby } = currentDrag
233
+
234
+ if (dir === 'l') {
235
+ const rx = (rtx + rbx) / 2
236
+ const ry = (rty + rby) / 2
237
+ width.value = Math.max((rx - x) * rotateCos.value + (ry - y) * rotateSin.value, minSize)
238
+ x0.value = rx - width.value * rotateCos.value / 2
239
+ y0.value = ry - width.value * rotateSin.value / 2
240
+ }
241
+ else if (dir === 'r') {
242
+ const lx = (ltx + lbx) / 2
243
+ const ly = (lty + lby) / 2
244
+ width.value = Math.max((x - lx) * rotateCos.value + (y - ly) * rotateSin.value, minSize)
245
+ x0.value = lx + width.value * rotateCos.value / 2
246
+ y0.value = ly + width.value * rotateSin.value / 2
247
+ }
248
+ else if (dir === 't') {
249
+ const bx = (lbx + rbx) / 2
250
+ const by = (lby + rby) / 2
251
+ height.value = Math.max((by - y) * rotateCos.value - (bx - x) * rotateSin.value, minSize)
252
+ x0.value = bx + height.value * rotateSin.value / 2
253
+ y0.value = by - height.value * rotateCos.value / 2
254
+ }
255
+ else if (dir === 'b') {
256
+ const tx = (ltx + rtx) / 2
257
+ const ty = (lty + rty) / 2
258
+ height.value = Math.max((y - ty) * rotateCos.value - (x - tx) * rotateSin.value, minSize)
259
+ x0.value = tx - height.value * rotateSin.value / 2
260
+ y0.value = ty + height.value * rotateCos.value / 2
261
+ }
262
+ },
263
+ onPointerup,
264
+ style: {
265
+ width: `${ctrlSize}px`,
266
+ height: `${ctrlSize}px`,
267
+ margin: `-${ctrlSize / 2}px`,
268
+ left: dir === 'l' ? '0' : dir === 'r' ? `100%` : `50%`,
269
+ top: dir === 't' ? '0' : dir === 'b' ? `100%` : `50%`,
270
+ cursor: 'lr'.includes(dir) ? 'ew-resize' : 'ns-resize',
271
+ borderRadius: '50%',
272
+ },
273
+ class: ctrlClasses,
274
+ }
275
+ }
276
+
277
+ function getRotateProps() {
278
+ return {
279
+ onPointerdown,
280
+ onPointermove: (ev: PointerEvent) => {
281
+ if (!currentDrag || ev.buttons !== 1)
282
+ return
283
+
284
+ ev.preventDefault()
285
+ ev.stopPropagation()
286
+
287
+ const x = (ev.clientX - slideLeft.value - currentDrag.dx0) / scale.value - ctrlSize / 4
288
+ const y = (ev.clientY - slideTop.value - currentDrag.dy0) / scale.value - ctrlSize / 4
289
+
290
+ let angle = Math.atan2(y - y0.value, x - x0.value) * 180 / Math.PI + 90
291
+
292
+ const commonAngles = [0, 90, 180, 270, 360]
293
+ for (const a of commonAngles) {
294
+ if (Math.abs(angle - a) < 5) {
295
+ angle = a % 360
296
+ break
297
+ }
298
+ }
299
+
300
+ rotate.value = angle
301
+ },
302
+ onPointerup,
303
+ style: {
304
+ width: `${ctrlSize}px`,
305
+ height: `${ctrlSize}px`,
306
+ margin: `-${ctrlSize / 2}px`,
307
+ left: '50%',
308
+ top: '-20px',
309
+ cursor: 'grab',
310
+ borderRadius: '50%',
311
+ },
312
+ class: ctrlClasses,
313
+ }
314
+ }
315
+
316
+ const moveInterval = 20
317
+ const intervalFnOptions = {
318
+ immediate: false,
319
+ immediateCallback: false,
320
+ }
321
+ const moveLeft = useIntervalFn(() => {
322
+ if (boundingRight.value <= minRemain)
323
+ return
324
+ x0.value--
325
+ }, moveInterval, intervalFnOptions)
326
+ const moveRight = useIntervalFn(() => {
327
+ if (boundingLeft.value >= slideWidth.value - minRemain)
328
+ return
329
+ x0.value++
330
+ }, moveInterval, intervalFnOptions)
331
+ const moveUp = useIntervalFn(() => {
332
+ if (boundingBottom.value <= minRemain)
333
+ return
334
+ y0.value--
335
+ }, moveInterval, intervalFnOptions)
336
+ const moveDown = useIntervalFn(() => {
337
+ if (boundingTop.value >= slideHeight.value - minRemain)
338
+ return
339
+ y0.value++
340
+ }, moveInterval, intervalFnOptions)
341
+
342
+ watchEffect(() => {
343
+ function shortcut(key: string, fn: Pausable) {
344
+ if (magicKeys[key].value)
345
+ fn.resume()
346
+ else fn.pause()
347
+ }
348
+ shortcut('left', moveLeft)
349
+ shortcut('right', moveRight)
350
+ shortcut('up', moveUp)
351
+ shortcut('down', moveDown)
352
+ })
353
+ </script>
354
+
355
+ <template>
356
+ <div
357
+ v-if="Number.isFinite(x0)"
358
+ :data-drag-id="id"
359
+ :style="{
360
+ position: 'absolute',
361
+ zIndex: 100,
362
+ left: `${zoom * (x0 - width / 2)}px`,
363
+ top: `${zoom * (y0 - height / 2)}px`,
364
+ width: `${zoom * width}px`,
365
+ height: `${zoom * height}px`,
366
+ transformOrigin: 'center center',
367
+ transform: `rotate(${rotate}deg)`,
368
+ }"
369
+ @pointerdown="onPointerdown"
370
+ @pointermove="onPointermove"
371
+ @pointerup="onPointerup"
372
+ >
373
+ <div class="absolute inset-0 z-100 b b-dark dark:b-gray-400">
374
+ <template v-if="!autoHeight">
375
+ <div v-bind="getCornerProps(true, true)" />
376
+ <div v-bind="getCornerProps(true, false)" />
377
+ <div v-bind="getCornerProps(false, true)" />
378
+ <div v-bind="getCornerProps(false, false)" />
379
+ </template>
380
+ <div v-bind="getBorderProps('l')" />
381
+ <div v-bind="getBorderProps('r')" />
382
+ <template v-if="!autoHeight">
383
+ <div v-bind="getBorderProps('t')" />
384
+ <div v-bind="getBorderProps('b')" />
385
+ </template>
386
+ <div v-bind="getRotateProps()" />
387
+ <div
388
+ class="absolute -top-15px w-0 b b-dashed b-dark dark:b-gray-400"
389
+ :style="{
390
+ left: 'calc(50% - 1px)',
391
+ height: autoHeight ? '14px' : '10px',
392
+ }"
393
+ />
394
+ </div>
395
+ </div>
396
+ </template>
@@ -2,7 +2,7 @@
2
2
  import { provideLocal, useElementSize, useStyleTag } from '@vueuse/core'
3
3
  import { computed, ref, watchEffect } from 'vue'
4
4
  import { configs, slideAspect, slideHeight, slideWidth } from '../env'
5
- import { injectionSlideScale } from '../constants'
5
+ import { injectionSlideElement, injectionSlideScale } from '../constants'
6
6
  import { useNav } from '../composables/useNav'
7
7
 
8
8
  const props = defineProps({
@@ -23,11 +23,12 @@ const props = defineProps({
23
23
 
24
24
  const { clicksDirection, isPrintMode } = useNav()
25
25
 
26
- const root = ref<HTMLDivElement>()
27
- const element = useElementSize(root)
26
+ const root = ref<HTMLDivElement | null>(null)
27
+ const rootSize = useElementSize(root)
28
+ const slideElement = ref<HTMLElement | null>(null)
28
29
 
29
- const width = computed(() => props.width ? props.width : element.width.value)
30
- const height = computed(() => props.width ? props.width / slideAspect.value : element.height.value)
30
+ const width = computed(() => props.width ? props.width : rootSize.width.value)
31
+ const height = computed(() => props.width ? props.width / slideAspect.value : rootSize.height.value)
31
32
 
32
33
  if (props.width) {
33
34
  watchEffect(() => {
@@ -42,7 +43,7 @@ const screenAspect = computed(() => width.value / height.value)
42
43
 
43
44
  const scale = computed(() => {
44
45
  if (props.scale && !isPrintMode.value)
45
- return props.scale
46
+ return +props.scale
46
47
  if (screenAspect.value < slideAspect.value)
47
48
  return width.value / slideWidth.value
48
49
  return height.value * slideAspect.value / slideWidth.value
@@ -69,12 +70,13 @@ if (props.isMain) {
69
70
  `))
70
71
  }
71
72
 
72
- provideLocal(injectionSlideScale, scale as any)
73
+ provideLocal(injectionSlideScale, scale)
74
+ provideLocal(injectionSlideElement, slideElement)
73
75
  </script>
74
76
 
75
77
  <template>
76
78
  <div id="slide-container" ref="root" class="slidev-slides-container" :class="className">
77
- <div id="slide-content" class="slidev-slide-content" :style="style">
79
+ <div id="slide-content" ref="slideElement" class="slidev-slide-content" :style="style">
78
80
  <slot />
79
81
  </div>
80
82
  <slot name="controls" />
@@ -4,6 +4,7 @@ import type { PropType } from 'vue'
4
4
  import { provideLocal } from '@vueuse/core'
5
5
  import type { ClicksContext, RenderContext, SlideRoute } from '@slidev/types'
6
6
  import { injectionActive, injectionClicksContext, injectionCurrentPage, injectionRenderContext, injectionRoute, injectionSlideZoom } from '../constants'
7
+ import { getSlideClass } from '../utils'
7
8
  import SlideLoading from './SlideLoading.vue'
8
9
 
9
10
  const props = defineProps({
@@ -67,16 +68,24 @@ const SlideComponent = defineAsyncComponent({
67
68
  </script>
68
69
 
69
70
  <template>
70
- <component
71
- :is="SlideComponent"
72
- :style="style"
73
- :data-slidev-no="props.route.no"
74
- :class="{ 'disable-view-transition': !['slide', 'presenter'].includes(props.renderContext) }"
75
- />
71
+ <div :class="getSlideClass(route)">
72
+ <component
73
+ :is="SlideComponent"
74
+ :style="style"
75
+ :data-slidev-no="props.route.no"
76
+ :class="{ 'disable-view-transition': !['slide', 'presenter'].includes(props.renderContext) }"
77
+ />
78
+ </div>
76
79
  </template>
77
80
 
78
81
  <style scoped>
79
82
  .disable-view-transition:deep(*) {
80
83
  view-transition-name: none !important;
81
84
  }
85
+
86
+ .slidev-page {
87
+ position: absolute;
88
+ width: 100%;
89
+ height: 100%;
90
+ }
82
91
  </style>
@@ -2,13 +2,14 @@
2
2
  import { TransitionGroup, computed, shallowRef, watch } from 'vue'
3
3
  import { recomputeAllPoppers } from 'floating-vue'
4
4
  import { useNav } from '../composables/useNav'
5
- import { getSlideClass } from '../utils'
6
5
  import { useViewTransition } from '../composables/useViewTransition'
7
6
  import { skipTransition } from '../logic/hmr'
8
7
  import { createFixedClicks } from '../composables/useClicks'
8
+ import { activeDragElement } from '../state'
9
9
  import { CLICKS_MAX } from '../constants'
10
10
  import SlideWrapper from './SlideWrapper.vue'
11
11
  import PresenterMouse from './PresenterMouse.vue'
12
+ import DragControl from './DragControl.vue'
12
13
 
13
14
  import GlobalTop from '#slidev/global-components/top'
14
15
  import GlobalBottom from '#slidev/global-components/bottom'
@@ -65,21 +66,19 @@ function onAfterLeave() {
65
66
  tag="div"
66
67
  @after-leave="onAfterLeave"
67
68
  >
68
- <div
69
+ <SlideWrapper
70
+ :is="route.component!"
69
71
  v-for="route of loadedRoutes"
70
72
  v-show="route === currentSlideRoute"
71
73
  :key="route.no"
72
- >
73
- <SlideWrapper
74
- :is="route.component!"
75
- :clicks-context="isPrintMode && !isPrintWithClicks ? createFixedClicks(route, CLICKS_MAX) : getPrimaryClicks(route)"
76
- :class="getSlideClass(route)"
77
- :route="route"
78
- :render-context="renderContext"
79
- />
80
- </div>
74
+ :clicks-context="isPrintMode && !isPrintWithClicks ? createFixedClicks(route, CLICKS_MAX) : getPrimaryClicks(route)"
75
+ :route="route"
76
+ :render-context="renderContext"
77
+ />
81
78
  </component>
82
79
 
80
+ <DragControl v-if="activeDragElement" :data="activeDragElement" />
81
+
83
82
  <div id="twoslash-container" />
84
83
 
85
84
  <!-- Global Top -->
@@ -95,10 +94,4 @@ function onAfterLeave() {
95
94
  #slideshow {
96
95
  height: 100%;
97
96
  }
98
-
99
- #slideshow > div {
100
- position: absolute;
101
- height: 100%;
102
- width: 100%;
103
- }
104
97
  </style>
@@ -0,0 +1,44 @@
1
+ import type { App } from 'vue'
2
+ import { watch } from 'vue'
3
+ import type { DragElementState } from '../composables/useDragElements'
4
+ import { useDragElement } from '../composables/useDragElements'
5
+
6
+ export function createVDragDirective() {
7
+ return {
8
+ install(app: App) {
9
+ app.directive<HTMLElement & { draggingState: DragElementState }>('drag', {
10
+ // @ts-expect-error extra prop
11
+ name: 'v-drag',
12
+
13
+ created(el, binding, vnode) {
14
+ const state = useDragElement(binding, binding.value, vnode.props?.markdownSource)
15
+ if (vnode.props) {
16
+ vnode.props = { ...vnode.props }
17
+ delete vnode.props.markdownSource
18
+ }
19
+ state.container.value = el
20
+ el.draggingState = state
21
+ el.dataset.dragId = state.id
22
+ state.watchStopHandles.push(
23
+ watch(state.containerStyle, (style) => {
24
+ for (const [k, v] of Object.entries(style)) {
25
+ if (v)
26
+ el.style[k as any] = v
27
+ }
28
+ }, { immediate: true }),
29
+ )
30
+ el.addEventListener('dblclick', state.startDragging)
31
+ },
32
+ mounted(el) {
33
+ el.draggingState.mounted()
34
+ },
35
+ unmounted(el) {
36
+ const state = el.draggingState
37
+ state.unmounted()
38
+ el.removeEventListener('dblclick', state.startDragging)
39
+ state.watchStopHandles.forEach(fn => fn())
40
+ },
41
+ })
42
+ },
43
+ }
44
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@slidev/client",
3
3
  "type": "module",
4
- "version": "0.48.9",
4
+ "version": "0.49.0-beta.1",
5
5
  "description": "Presentation slides for developers",
6
6
  "author": "antfu <anthonyfu117@hotmail.com>",
7
7
  "license": "MIT",
@@ -30,14 +30,14 @@
30
30
  "dependencies": {
31
31
  "@antfu/utils": "^0.7.7",
32
32
  "@iconify-json/carbon": "^1.1.31",
33
- "@iconify-json/ph": "^1.1.11",
33
+ "@iconify-json/ph": "^1.1.12",
34
34
  "@iconify-json/svg-spinners": "^1.1.2",
35
- "@shikijs/monaco": "^1.2.3",
36
- "@shikijs/vitepress-twoslash": "^1.2.3",
35
+ "@shikijs/monaco": "^1.3.0",
36
+ "@shikijs/vitepress-twoslash": "^1.3.0",
37
37
  "@slidev/rough-notation": "^0.1.0",
38
38
  "@typescript/ata": "^0.9.4",
39
39
  "@unhead/vue": "^1.9.4",
40
- "@unocss/reset": "^0.58.9",
40
+ "@unocss/reset": "^0.59.0",
41
41
  "@vueuse/core": "^10.9.0",
42
42
  "@vueuse/math": "^10.9.0",
43
43
  "@vueuse/motion": "^2.1.0",
@@ -53,17 +53,17 @@
53
53
  "monaco-editor": "^0.47.0",
54
54
  "prettier": "^3.2.5",
55
55
  "recordrtc": "^5.6.2",
56
- "shiki": "^1.2.3",
56
+ "shiki": "^1.3.0",
57
57
  "shiki-magic-move": "^0.3.5",
58
- "typescript": "^5.4.3",
59
- "unocss": "^0.58.9",
58
+ "typescript": "^5.4.4",
59
+ "unocss": "^0.59.0",
60
60
  "vue": "^3.4.21",
61
61
  "vue-demi": "^0.14.7",
62
62
  "vue-router": "^4.3.0",
63
- "@slidev/parser": "0.48.9",
64
- "@slidev/types": "0.48.9"
63
+ "@slidev/parser": "0.49.0-beta.1",
64
+ "@slidev/types": "0.49.0-beta.1"
65
65
  },
66
66
  "devDependencies": {
67
- "vite": "^5.2.7"
67
+ "vite": "^5.2.8"
68
68
  }
69
69
  }
package/setup/main.ts CHANGED
@@ -7,6 +7,7 @@ import { createHead } from '@unhead/vue'
7
7
  import { routeForceRefresh } from '../logic/route'
8
8
  import { createVClickDirectives } from '../modules/v-click'
9
9
  import { createVMarkDirective } from '../modules/v-mark'
10
+ import { createVDragDirective } from '../modules/v-drag'
10
11
  import { createVMotionDirectives } from '../modules/v-motion'
11
12
  import { routes } from '../routes'
12
13
  import setups from '#slidev/setups/main'
@@ -34,6 +35,7 @@ export default async function setupMain(app: App) {
34
35
  app.use(createHead())
35
36
  app.use(createVClickDirectives())
36
37
  app.use(createVMarkDirective())
38
+ app.use(createVDragDirective())
37
39
  app.use(createVMotionDirectives())
38
40
  app.use(TwoSlashFloatingVue as any, { container: '#twoslash-container' })
39
41
 
@@ -2,7 +2,7 @@ import { and, not, or } from '@vueuse/math'
2
2
  import type { NavOperations, ShortcutOptions } from '@slidev/types'
3
3
  import { downloadPDF } from '../utils'
4
4
  import { toggleDark } from '../logic/dark'
5
- import { magicKeys, showGotoDialog, showOverview, toggleOverview } from '../state'
5
+ import { activeDragElement, magicKeys, showGotoDialog, showOverview, toggleOverview } from '../state'
6
6
  import { useNav } from '../composables/useNav'
7
7
  import { useDrawings } from '../composables/useDrawings'
8
8
  import { currentOverviewPage, downOverviewPage, nextOverviewPage, prevOverviewPage, upOverviewPage } from './../logic/overview'
@@ -29,15 +29,17 @@ export default function setupShortcuts() {
29
29
  showGotoDialog: () => showGotoDialog.value = !showGotoDialog.value,
30
30
  }
31
31
 
32
+ const navViaArrowKeys = and(not(showOverview), not(activeDragElement))
33
+
32
34
  let shortcuts: ShortcutOptions[] = [
33
35
  { name: 'next_space', key: and(space, not(shift)), fn: next, autoRepeat: true },
34
36
  { name: 'prev_space', key: and(space, shift), fn: prev, autoRepeat: true },
35
- { name: 'next_right', key: and(right, not(shift), not(showOverview)), fn: next, autoRepeat: true },
36
- { name: 'prev_left', key: and(left, not(shift), not(showOverview)), fn: prev, autoRepeat: true },
37
+ { name: 'next_right', key: and(right, not(shift), navViaArrowKeys), fn: next, autoRepeat: true },
38
+ { name: 'prev_left', key: and(left, not(shift), navViaArrowKeys), fn: prev, autoRepeat: true },
37
39
  { name: 'next_page_key', key: 'pageDown', fn: next, autoRepeat: true },
38
40
  { name: 'prev_page_key', key: 'pageUp', fn: prev, autoRepeat: true },
39
- { name: 'next_down', key: and(down, not(showOverview)), fn: nextSlide, autoRepeat: true },
40
- { name: 'prev_up', key: and(up, not(showOverview)), fn: () => prevSlide(false), autoRepeat: true },
41
+ { name: 'next_down', key: and(down, navViaArrowKeys), fn: nextSlide, autoRepeat: true },
42
+ { name: 'prev_up', key: and(up, navViaArrowKeys), fn: () => prevSlide(false), autoRepeat: true },
41
43
  { name: 'next_shift', key: and(right, shift), fn: nextSlide, autoRepeat: true },
42
44
  { name: 'prev_shift', key: and(left, shift), fn: () => prevSlide(false), autoRepeat: true },
43
45
  { name: 'toggle_dark', key: and(d, not(drawingEnabled)), fn: toggleDark },
package/state/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { breakpointsTailwind, isClient, useActiveElement, useBreakpoints, useFullscreen, useLocalStorage, useMagicKeys, useToggle, useWindowSize } from '@vueuse/core'
2
- import { computed, ref } from 'vue'
2
+ import { computed, ref, shallowRef } from 'vue'
3
3
  import { slideAspect } from '../env'
4
+ import type { DragElementState } from '../composables/useDragElements'
4
5
 
5
6
  export const showRecordingDialog = ref(false)
6
7
  export const showInfoDialog = ref(false)
@@ -31,6 +32,8 @@ export const isEditorVertical = useLocalStorage('slidev-editor-vertical', false,
31
32
  export const editorWidth = useLocalStorage('slidev-editor-width', isClient ? window.innerWidth * 0.4 : 318, { listenToStorageChanges: false })
32
33
  export const editorHeight = useLocalStorage('slidev-editor-height', isClient ? window.innerHeight * 0.4 : 300, { listenToStorageChanges: false })
33
34
 
35
+ export const activeDragElement = shallowRef<DragElementState | null>(null)
36
+
34
37
  export const presenterNotesFontSize = useLocalStorage('slidev-presenter-font-size', 1, { listenToStorageChanges: false })
35
38
  export const presenterLayout = useLocalStorage('slidev-presenter-layout', 1, { listenToStorageChanges: false })
36
39