@slidev/client 0.48.9 → 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.
- package/builtin/CodeBlockWrapper.vue +3 -3
- package/builtin/KaTexBlockWrapper.vue +3 -3
- package/builtin/Monaco.vue +3 -0
- package/builtin/ShikiMagicMove.vue +3 -3
- package/builtin/SlidevVideo.vue +53 -65
- package/builtin/VClicks.ts +7 -2
- package/builtin/VDrag.vue +27 -0
- package/composables/useClicks.ts +80 -32
- package/composables/useDragElements.ts +286 -0
- package/composables/useNav.ts +20 -0
- package/composables/useSlideBounds.ts +30 -0
- package/composables/useSlideInfo.ts +12 -5
- package/constants.ts +3 -1
- package/env.ts +2 -0
- package/internals/CodeRunner.vue +26 -3
- package/internals/ContextMenu.vue +110 -0
- package/internals/Controls.vue +2 -0
- package/internals/DragControl.vue +397 -0
- package/internals/NavControls.vue +6 -11
- package/internals/SlideContainer.vue +10 -8
- package/internals/SlideWrapper.vue +15 -6
- package/internals/SlidesShow.vue +10 -17
- package/logic/contextMenu.ts +34 -0
- package/logic/utils.ts +0 -18
- package/modules/v-click.ts +34 -70
- package/modules/v-drag.ts +44 -0
- package/modules/v-mark.ts +3 -3
- package/modules/v-motion.ts +15 -26
- package/package.json +13 -13
- package/pages/play.vue +4 -2
- package/pages/presenter.vue +4 -0
- package/setup/code-runners.ts +7 -4
- package/setup/context-menu.ts +113 -0
- package/setup/main.ts +2 -0
- package/setup/shortcuts.ts +7 -5
- package/state/index.ts +4 -1
|
@@ -0,0 +1,286 @@
|
|
|
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
|
+
dragPos: frontmatter.dragPos,
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
if (!markdownSource)
|
|
62
|
+
throw new Error(`[Slidev] VDrag Element ${id} is missing markdown source`)
|
|
63
|
+
|
|
64
|
+
const [startLine, endLine, idx] = markdownSource
|
|
65
|
+
const lines = info.value.content.split(/\r?\n/g)
|
|
66
|
+
|
|
67
|
+
let section = lines.slice(startLine, endLine).join('\n')
|
|
68
|
+
let replaced = false
|
|
69
|
+
|
|
70
|
+
section = type === 'prop'
|
|
71
|
+
? section.replace(/<(v-?drag)(.*?)>/ig, (full, tag, attrs, index) => {
|
|
72
|
+
if (index === idx) {
|
|
73
|
+
replaced = true
|
|
74
|
+
const posMatch = attrs.match(/pos=".*?"/)
|
|
75
|
+
if (!posMatch)
|
|
76
|
+
return `<${tag}${ensureSuffix(' ', attrs)}pos="${posStr}">`
|
|
77
|
+
const start = posMatch.index
|
|
78
|
+
const end = start + posMatch[0].length
|
|
79
|
+
return `<${tag}${attrs.slice(0, start)}pos="${posStr}"${attrs.slice(end)}>`
|
|
80
|
+
}
|
|
81
|
+
return full
|
|
82
|
+
})
|
|
83
|
+
: section.replace(/(?<![</\w])v-drag(?:=".*?")?/ig, (full, index) => {
|
|
84
|
+
if (index === idx) {
|
|
85
|
+
replaced = true
|
|
86
|
+
return `v-drag="${posStr}"`
|
|
87
|
+
}
|
|
88
|
+
return full
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
if (!replaced)
|
|
92
|
+
throw new Error(`[Slidev] VDrag Element ${id} is not found in the markdown source`)
|
|
93
|
+
|
|
94
|
+
lines.splice(
|
|
95
|
+
startLine,
|
|
96
|
+
endLine - startLine,
|
|
97
|
+
section,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
const newContent = lines.join('\n')
|
|
101
|
+
if (info.value.content === newContent)
|
|
102
|
+
return
|
|
103
|
+
newPatch = {
|
|
104
|
+
content: newContent,
|
|
105
|
+
}
|
|
106
|
+
info.value = {
|
|
107
|
+
...info.value,
|
|
108
|
+
content: newContent,
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
debouncedSave()
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function useDragElement(directive: DirectiveBinding | null, posRaw?: string | number | number[], markdownSource?: DragElementMarkdownSource) {
|
|
116
|
+
function inject<T>(key: InjectionKey<T> | string): T | undefined {
|
|
117
|
+
return directive
|
|
118
|
+
? directiveInject(directive, key)
|
|
119
|
+
: injectLocal(key)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const renderContext = inject(injectionRenderContext)!
|
|
123
|
+
const frontmatter = inject(injectionFrontmatter) ?? {}
|
|
124
|
+
const page = inject(injectionCurrentPage)!
|
|
125
|
+
const updater = computed(() => useDragElementsUpdater(page.value))
|
|
126
|
+
const scale = inject(injectionSlideScale) ?? ref(1)
|
|
127
|
+
const zoom = inject(injectionSlideZoom) ?? ref(1)
|
|
128
|
+
const { left: slideLeft, top: slideTop, stop: stopWatchBounds } = useSlideBounds(inject(injectionSlideElement) ?? ref())
|
|
129
|
+
const enabled = ['slide', 'presenter'].includes(renderContext.value)
|
|
130
|
+
|
|
131
|
+
let dataSource: DragElementDataSource = directive ? 'directive' : 'prop'
|
|
132
|
+
let id: string = makeId()
|
|
133
|
+
let pos: number[] | undefined
|
|
134
|
+
if (Array.isArray(posRaw)) {
|
|
135
|
+
pos = posRaw
|
|
136
|
+
}
|
|
137
|
+
else if (typeof posRaw === 'string' && posRaw.includes(',')) {
|
|
138
|
+
pos = posRaw.split(',').map(Number)
|
|
139
|
+
}
|
|
140
|
+
else if (posRaw != null) {
|
|
141
|
+
dataSource = 'frontmatter'
|
|
142
|
+
id = `${posRaw}`
|
|
143
|
+
posRaw = frontmatter?.dragPos?.[id]
|
|
144
|
+
pos = (posRaw as string)?.split(',').map(Number)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (dataSource !== 'frontmatter' && !markdownSource)
|
|
148
|
+
throw new Error('[Slidev] Can not identify the source position of the v-drag element, please provide an explicit `id` prop.')
|
|
149
|
+
|
|
150
|
+
const watchStopHandles: WatchStopHandle[] = [stopWatchBounds]
|
|
151
|
+
|
|
152
|
+
const autoHeight = posRaw != null && !Number.isFinite(pos?.[3])
|
|
153
|
+
pos ??= [Number.NaN, Number.NaN, 0]
|
|
154
|
+
const width = ref(pos[2])
|
|
155
|
+
const x0 = ref(pos[0] + pos[2] / 2)
|
|
156
|
+
|
|
157
|
+
const rotate = ref(pos[4] ?? 0)
|
|
158
|
+
const rotateRad = computed(() => rotate.value * Math.PI / 180)
|
|
159
|
+
const rotateSin = computed(() => Math.sin(rotateRad.value))
|
|
160
|
+
const rotateCos = computed(() => Math.cos(rotateRad.value))
|
|
161
|
+
|
|
162
|
+
const container = ref<HTMLElement>()
|
|
163
|
+
const bounds = ref({ left: 0, top: 0, width: 0, height: 0 })
|
|
164
|
+
const actualHeight = ref(0)
|
|
165
|
+
function updateBounds() {
|
|
166
|
+
const rect = container.value!.getBoundingClientRect()
|
|
167
|
+
bounds.value = {
|
|
168
|
+
left: rect.left / zoom.value,
|
|
169
|
+
top: rect.top / zoom.value,
|
|
170
|
+
width: rect.width / zoom.value,
|
|
171
|
+
height: rect.height / zoom.value,
|
|
172
|
+
}
|
|
173
|
+
actualHeight.value = ((bounds.value.width + bounds.value.height) / scale.value / (Math.abs(rotateSin.value) + Math.abs(rotateCos.value)) - width.value)
|
|
174
|
+
}
|
|
175
|
+
watchStopHandles.push(watch(width, updateBounds, { flush: 'post' }))
|
|
176
|
+
|
|
177
|
+
const configuredHeight = ref(pos[3] ?? 0)
|
|
178
|
+
const height = computed({
|
|
179
|
+
get: () => (autoHeight ? actualHeight.value : configuredHeight.value) || 0,
|
|
180
|
+
set: v => !autoHeight && (configuredHeight.value = v),
|
|
181
|
+
})
|
|
182
|
+
const configuredY0 = ref(pos[1])
|
|
183
|
+
const y0 = computed({
|
|
184
|
+
get: () => configuredY0.value + height.value / 2,
|
|
185
|
+
set: v => configuredY0.value = v - height.value / 2,
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
const containerStyle = computed<CSSProperties>(() => {
|
|
189
|
+
return Number.isFinite(x0.value)
|
|
190
|
+
? {
|
|
191
|
+
position: 'absolute',
|
|
192
|
+
zIndex: 100,
|
|
193
|
+
left: `${x0.value - width.value / 2}px`,
|
|
194
|
+
top: `${y0.value - height.value / 2}px`,
|
|
195
|
+
width: `${width.value}px`,
|
|
196
|
+
height: autoHeight ? undefined : `${height.value}px`,
|
|
197
|
+
transformOrigin: 'center center',
|
|
198
|
+
transform: `rotate(${rotate.value}deg)`,
|
|
199
|
+
}
|
|
200
|
+
: {
|
|
201
|
+
position: 'absolute',
|
|
202
|
+
zIndex: 100,
|
|
203
|
+
}
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
watchStopHandles.push(
|
|
207
|
+
watch(
|
|
208
|
+
[x0, y0, width, height, rotate],
|
|
209
|
+
([x0, y0, w, h, r]) => {
|
|
210
|
+
let posStr = [x0 - w / 2, y0 - h / 2, w].map(Math.round).join()
|
|
211
|
+
if (autoHeight)
|
|
212
|
+
posStr += dataSource === 'directive' ? ',NaN' : ',_'
|
|
213
|
+
else
|
|
214
|
+
posStr += `,${Math.round(h)}`
|
|
215
|
+
if (Math.round(r) !== 0)
|
|
216
|
+
posStr += `,${Math.round(r)}`
|
|
217
|
+
|
|
218
|
+
if (dataSource === 'directive')
|
|
219
|
+
posStr = `[${posStr}]`
|
|
220
|
+
|
|
221
|
+
updater.value(id, posStr, dataSource, markdownSource)
|
|
222
|
+
},
|
|
223
|
+
),
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
const state = {
|
|
227
|
+
id,
|
|
228
|
+
dataSource,
|
|
229
|
+
markdownSource,
|
|
230
|
+
zoom,
|
|
231
|
+
autoHeight,
|
|
232
|
+
x0,
|
|
233
|
+
y0,
|
|
234
|
+
width,
|
|
235
|
+
height,
|
|
236
|
+
rotate,
|
|
237
|
+
container,
|
|
238
|
+
containerStyle,
|
|
239
|
+
watchStopHandles,
|
|
240
|
+
dragging: computed((): boolean => activeDragElement.value === state),
|
|
241
|
+
mounted() {
|
|
242
|
+
if (!enabled)
|
|
243
|
+
return
|
|
244
|
+
updateBounds()
|
|
245
|
+
if (!posRaw) {
|
|
246
|
+
setTimeout(() => {
|
|
247
|
+
updateBounds()
|
|
248
|
+
x0.value = (bounds.value.left + bounds.value.width / 2 - slideLeft.value) / scale.value
|
|
249
|
+
y0.value = (bounds.value.top - slideTop.value) / scale.value
|
|
250
|
+
width.value = bounds.value.width / scale.value
|
|
251
|
+
height.value = bounds.value.height / scale.value
|
|
252
|
+
}, 100)
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
unmounted() {
|
|
256
|
+
if (!enabled)
|
|
257
|
+
return
|
|
258
|
+
state.stopDragging()
|
|
259
|
+
},
|
|
260
|
+
startDragging(): void {
|
|
261
|
+
updateBounds()
|
|
262
|
+
activeDragElement.value = state
|
|
263
|
+
},
|
|
264
|
+
stopDragging(): void {
|
|
265
|
+
if (activeDragElement.value === state)
|
|
266
|
+
activeDragElement.value = null
|
|
267
|
+
},
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
watchStopHandles.push(
|
|
271
|
+
onClickOutside(container, (ev) => {
|
|
272
|
+
const container = document.querySelector('#drag-control-container')
|
|
273
|
+
if (container && ev.target && container.contains(ev.target as HTMLElement))
|
|
274
|
+
return
|
|
275
|
+
state.stopDragging()
|
|
276
|
+
}),
|
|
277
|
+
watch(useWindowFocus(), (focused) => {
|
|
278
|
+
if (!focused)
|
|
279
|
+
state.stopDragging()
|
|
280
|
+
}),
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
return state
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export type DragElementState = ReturnType<typeof useDragElement>
|
package/composables/useNav.ts
CHANGED
|
@@ -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
|
|
|
@@ -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,7 +14,6 @@ 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>>
|
|
16
|
-
export const injectionClickVisibility = '$$slidev-click-visibility' as unknown as InjectionKey<ComputedRef<true | 'before' | 'after'>>
|
|
17
17
|
|
|
18
18
|
export const CLASS_VCLICK_TARGET = 'slidev-vclick-target'
|
|
19
19
|
export const CLASS_VCLICK_HIDDEN = 'slidev-vclick-hidden'
|
|
@@ -44,6 +44,7 @@ export const FRONTMATTER_FIELDS = [
|
|
|
44
44
|
'title',
|
|
45
45
|
'transition',
|
|
46
46
|
'zoom',
|
|
47
|
+
'dragPos',
|
|
47
48
|
]
|
|
48
49
|
|
|
49
50
|
export const HEADMATTER_FIELDS = [
|
|
@@ -75,4 +76,5 @@ export const HEADMATTER_FIELDS = [
|
|
|
75
76
|
'drawings',
|
|
76
77
|
'htmlAttrs',
|
|
77
78
|
'mdc',
|
|
79
|
+
'contextMenu',
|
|
78
80
|
]
|
package/env.ts
CHANGED
package/internals/CodeRunner.vue
CHANGED
|
@@ -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>
|
package/internals/Controls.vue
CHANGED
|
@@ -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>
|