@slidev/client 0.48.8 → 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.
- package/builtin/ShikiMagicMove.vue +34 -25
- package/builtin/SlidevVideo.vue +54 -62
- package/builtin/VDrag.vue +27 -0
- package/composables/useDragElements.ts +282 -0
- package/composables/useSlideBounds.ts +30 -0
- package/composables/useSlideInfo.ts +12 -5
- package/constants.ts +3 -0
- package/internals/DragControl.vue +396 -0
- package/internals/SlideContainer.vue +10 -8
- package/internals/SlideWrapper.vue +15 -6
- package/internals/SlidesShow.vue +10 -18
- package/modules/v-click.ts +40 -21
- package/modules/v-drag.ts +44 -0
- package/modules/v-mark.ts +7 -1
- package/modules/v-motion.ts +120 -0
- package/package.json +13 -13
- package/setup/code-runners.ts +94 -10
- package/setup/main.ts +4 -2
- package/setup/shortcuts.ts +7 -5
- package/state/index.ts +4 -1
- package/utils.ts +16 -0
|
@@ -45,43 +45,45 @@ onMounted(() => {
|
|
|
45
45
|
// Calculate the step and rangeStr based on the current click count
|
|
46
46
|
const clickCount = clicks.current - start
|
|
47
47
|
let step = steps.length - 1
|
|
48
|
-
let
|
|
48
|
+
let currentClickSum = 0
|
|
49
49
|
let rangeStr = 'all'
|
|
50
50
|
for (let i = 0; i < ranges.value.length; i++) {
|
|
51
51
|
const current = ranges.value[i]
|
|
52
|
-
if (clickCount <
|
|
52
|
+
if (clickCount < currentClickSum + current.length - 1) {
|
|
53
53
|
step = i
|
|
54
|
-
rangeStr = current[clickCount -
|
|
54
|
+
rangeStr = current[clickCount - currentClickSum + 1]
|
|
55
55
|
break
|
|
56
56
|
}
|
|
57
|
-
|
|
57
|
+
currentClickSum += current.length || 1
|
|
58
58
|
}
|
|
59
59
|
stepIndex.value = step
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
setTimeout(() => {
|
|
62
|
+
const pre = container.value?.querySelector('.shiki') as HTMLElement
|
|
63
|
+
if (!pre)
|
|
64
|
+
return
|
|
64
65
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
const children = (Array.from(pre.children) as HTMLElement[])
|
|
67
|
+
.slice(1) // Remove the first anchor
|
|
68
|
+
.filter(i => !i.className.includes('shiki-magic-move-leave')) // Filter the leaving elements
|
|
68
69
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
70
|
+
// Group to lines between `<br>`
|
|
71
|
+
const lines = children.reduce((acc, el) => {
|
|
72
|
+
if (el.tagName === 'BR')
|
|
73
|
+
acc.push([])
|
|
74
|
+
else
|
|
75
|
+
acc[acc.length - 1].push(el)
|
|
76
|
+
return acc
|
|
77
|
+
}, [[]] as HTMLElement[][])
|
|
77
78
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
79
|
+
// Update highlight range
|
|
80
|
+
updateCodeHighlightRange(
|
|
81
|
+
rangeStr,
|
|
82
|
+
lines.length,
|
|
83
|
+
1,
|
|
84
|
+
no => lines[no],
|
|
85
|
+
)
|
|
86
|
+
})
|
|
85
87
|
},
|
|
86
88
|
{ immediate: true },
|
|
87
89
|
)
|
|
@@ -104,3 +106,10 @@ onMounted(() => {
|
|
|
104
106
|
/>
|
|
105
107
|
</div>
|
|
106
108
|
</template>
|
|
109
|
+
|
|
110
|
+
<style>
|
|
111
|
+
.slidev-code-magic-move .shiki-magic-move-enter-from,
|
|
112
|
+
.slidev-code-magic-move .shiki-magic-move-leave-to {
|
|
113
|
+
opacity: 0;
|
|
114
|
+
}
|
|
115
|
+
</style>
|
package/builtin/SlidevVideo.vue
CHANGED
|
@@ -1,86 +1,78 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed, onMounted,
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
|
14
|
-
$renderContext
|
|
15
|
-
$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
|
-
|
|
52
|
-
if (
|
|
33
|
+
onMounted(() => {
|
|
34
|
+
if (noPlay.value)
|
|
53
35
|
return
|
|
54
36
|
|
|
55
|
-
|
|
56
|
-
|
|
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
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
|
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 |
|
|
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 |
|
|
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(
|
|
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>
|
|
@@ -13,6 +14,7 @@ export const injectionRenderContext = '$$slidev-render-context' as unknown as In
|
|
|
13
14
|
export const injectionActive = '$$slidev-active' as unknown as InjectionKey<Ref<boolean>>
|
|
14
15
|
export const injectionFrontmatter = '$$slidev-fontmatter' as unknown as InjectionKey<Record<string, any>>
|
|
15
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'>>
|
|
16
18
|
|
|
17
19
|
export const CLASS_VCLICK_TARGET = 'slidev-vclick-target'
|
|
18
20
|
export const CLASS_VCLICK_HIDDEN = 'slidev-vclick-hidden'
|
|
@@ -43,6 +45,7 @@ export const FRONTMATTER_FIELDS = [
|
|
|
43
45
|
'title',
|
|
44
46
|
'transition',
|
|
45
47
|
'zoom',
|
|
48
|
+
'dragPos',
|
|
46
49
|
]
|
|
47
50
|
|
|
48
51
|
export const HEADMATTER_FIELDS = [
|