@slidev/client 0.49.0-beta.1 → 0.49.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/builtin/CodeBlockWrapper.vue +3 -3
- package/builtin/KaTexBlockWrapper.vue +3 -3
- package/builtin/Monaco.vue +27 -4
- package/builtin/ShikiMagicMove.vue +3 -3
- package/builtin/SlidevVideo.vue +3 -7
- package/builtin/VClicks.ts +7 -2
- package/composables/useClicks.ts +80 -32
- package/composables/useDragElements.ts +7 -3
- package/composables/useNav.ts +20 -0
- package/constants.ts +1 -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 +1 -0
- package/internals/NavControls.vue +6 -11
- package/internals/QuickOverview.vue +6 -0
- package/logic/contextMenu.ts +34 -0
- package/logic/utils.ts +0 -18
- package/modules/v-click.ts +34 -70
- package/modules/v-mark.ts +3 -3
- package/modules/v-motion.ts +15 -26
- package/package.json +8 -8
- 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 -2
- package/setup/monaco.ts +0 -3
- package/setup/routes.ts +80 -0
- package/routes.ts +0 -68
|
@@ -63,10 +63,10 @@ onMounted(() => {
|
|
|
63
63
|
if (!clicks || !props.ranges?.length)
|
|
64
64
|
return
|
|
65
65
|
|
|
66
|
-
const
|
|
67
|
-
clicks.register(id,
|
|
66
|
+
const clicksInfo = clicks.calculateSince(props.at, props.ranges.length - 1)
|
|
67
|
+
clicks.register(id, clicksInfo)
|
|
68
68
|
|
|
69
|
-
const index = computed(() => Math.max(0, clicks.current - start + 1))
|
|
69
|
+
const index = computed(() => Math.max(0, clicks.current - clicksInfo.start + 1))
|
|
70
70
|
|
|
71
71
|
const finallyRange = computed(() => {
|
|
72
72
|
return props.finally === 'last' ? props.ranges.at(-1) : props.finally.toString()
|
|
@@ -58,10 +58,10 @@ onMounted(() => {
|
|
|
58
58
|
if (!clicks || !props.ranges?.length)
|
|
59
59
|
return
|
|
60
60
|
|
|
61
|
-
const
|
|
62
|
-
clicks.register(id,
|
|
61
|
+
const clicksInfo = clicks.calculateSince(props.at, props.ranges.length - 1)
|
|
62
|
+
clicks.register(id, clicksInfo)
|
|
63
63
|
|
|
64
|
-
const index = computed(() => Math.max(0, clicks.current - start + 1))
|
|
64
|
+
const index = computed(() => Math.max(0, clicks.current - clicksInfo.start + 1))
|
|
65
65
|
|
|
66
66
|
const finallyRange = computed(() => {
|
|
67
67
|
return props.finally === 'last' ? props.ranges.at(-1) : props.finally.toString()
|
package/builtin/Monaco.vue
CHANGED
|
@@ -16,7 +16,11 @@ import { debounce } from '@antfu/utils'
|
|
|
16
16
|
import lz from 'lz-string'
|
|
17
17
|
import type * as monaco from 'monaco-editor'
|
|
18
18
|
import { computed, nextTick, onMounted, ref } from 'vue'
|
|
19
|
+
import type { RawAtValue } from '@slidev/types'
|
|
20
|
+
import { whenever } from '@vueuse/core'
|
|
19
21
|
import { makeId } from '../logic/utils'
|
|
22
|
+
import { useSlideContext } from '../context'
|
|
23
|
+
import { useNav } from '../composables/useNav'
|
|
20
24
|
import CodeRunner from '../internals/CodeRunner.vue'
|
|
21
25
|
|
|
22
26
|
const props = withDefaults(defineProps<{
|
|
@@ -30,6 +34,7 @@ const props = withDefaults(defineProps<{
|
|
|
30
34
|
ata?: boolean
|
|
31
35
|
runnable?: boolean
|
|
32
36
|
autorun?: boolean | 'once'
|
|
37
|
+
showOutputAt?: RawAtValue
|
|
33
38
|
outputHeight?: string
|
|
34
39
|
highlightOutput?: boolean
|
|
35
40
|
runnerOptions?: Record<string, unknown>
|
|
@@ -74,6 +79,19 @@ const height = computed(() => {
|
|
|
74
79
|
return props.height
|
|
75
80
|
})
|
|
76
81
|
|
|
82
|
+
const loadTypes = ref<() => void>()
|
|
83
|
+
const { $page: thisSlideNo, $renderContext: renderContext } = useSlideContext()
|
|
84
|
+
const { currentSlideNo } = useNav()
|
|
85
|
+
const stopWatchTypesLoading = whenever(
|
|
86
|
+
() => Math.abs(thisSlideNo.value - currentSlideNo.value) <= 1 && loadTypes.value,
|
|
87
|
+
(loadTypes) => {
|
|
88
|
+
if (['slide', 'presenter'].includes(renderContext.value))
|
|
89
|
+
loadTypes()
|
|
90
|
+
else
|
|
91
|
+
setTimeout(loadTypes, 5000)
|
|
92
|
+
},
|
|
93
|
+
)
|
|
94
|
+
|
|
77
95
|
onMounted(async () => {
|
|
78
96
|
// Lazy load monaco, so it will be bundled in async chunk
|
|
79
97
|
const { default: setup } = await import('../setup/monaco')
|
|
@@ -135,11 +153,15 @@ onMounted(async () => {
|
|
|
135
153
|
})
|
|
136
154
|
editableEditor = editor
|
|
137
155
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
156
|
+
loadTypes.value = () => {
|
|
157
|
+
stopWatchTypesLoading()
|
|
158
|
+
import('#slidev/monaco-types')
|
|
159
|
+
if (props.ata) {
|
|
141
160
|
ata(editableEditor.getValue())
|
|
142
|
-
|
|
161
|
+
editableEditor.onDidChangeModelContent(debounce(1000, () => {
|
|
162
|
+
ata(editableEditor.getValue())
|
|
163
|
+
}))
|
|
164
|
+
}
|
|
143
165
|
}
|
|
144
166
|
const originalLayoutContentWidget = editableEditor.layoutContentWidget.bind(editableEditor)
|
|
145
167
|
editableEditor.layoutContentWidget = (widget: any) => {
|
|
@@ -165,6 +187,7 @@ onMounted(async () => {
|
|
|
165
187
|
v-model="code"
|
|
166
188
|
:lang="lang"
|
|
167
189
|
:autorun="props.autorun"
|
|
190
|
+
:show-output-at="props.showOutputAt"
|
|
168
191
|
:height="props.outputHeight"
|
|
169
192
|
:highlight-output="props.highlightOutput"
|
|
170
193
|
:runner-options="props.runnerOptions"
|
|
@@ -36,14 +36,14 @@ onMounted(() => {
|
|
|
36
36
|
throw new Error('[slidev] The length of stepRanges does not match the length of steps, this is an internal error.')
|
|
37
37
|
|
|
38
38
|
const clickCounts = ranges.value.map(s => s.length).reduce((a, b) => a + b, 0)
|
|
39
|
-
const
|
|
40
|
-
clicks.register(id,
|
|
39
|
+
const clickInfo = clicks.calculateSince(props.at ?? '+1', clickCounts - 1)
|
|
40
|
+
clicks.register(id, clickInfo)
|
|
41
41
|
|
|
42
42
|
watch(
|
|
43
43
|
() => clicks.current,
|
|
44
44
|
() => {
|
|
45
45
|
// Calculate the step and rangeStr based on the current click count
|
|
46
|
-
const clickCount = clicks.current - start
|
|
46
|
+
const clickCount = clicks.current - clickInfo.start
|
|
47
47
|
let step = steps.length - 1
|
|
48
48
|
let currentClickSum = 0
|
|
49
49
|
let rangeStr = 'all'
|
package/builtin/SlidevVideo.vue
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { computed, onMounted, ref, watch } from 'vue'
|
|
3
3
|
import { and } from '@vueuse/math'
|
|
4
4
|
import { useSlideContext } from '../context'
|
|
5
|
+
import { resolvedClickMap } from '../modules/v-click'
|
|
5
6
|
import { useNav } from '../composables/useNav'
|
|
6
7
|
|
|
7
8
|
const props = defineProps<{
|
|
@@ -17,12 +18,7 @@ const props = defineProps<{
|
|
|
17
18
|
const printPoster = computed(() => props.printPoster ?? props.poster)
|
|
18
19
|
const printTimestamp = computed(() => props.printTimestamp ?? props.timestamp ?? 0)
|
|
19
20
|
|
|
20
|
-
const {
|
|
21
|
-
$slidev,
|
|
22
|
-
$clicksContext,
|
|
23
|
-
$renderContext,
|
|
24
|
-
$route,
|
|
25
|
-
} = useSlideContext()
|
|
21
|
+
const { $slidev, $renderContext, $route } = useSlideContext()
|
|
26
22
|
const { isPrintMode } = useNav()
|
|
27
23
|
|
|
28
24
|
const noPlay = computed(() => isPrintMode.value || !['slide', 'presenter'].includes($renderContext.value))
|
|
@@ -38,7 +34,7 @@ onMounted(() => {
|
|
|
38
34
|
video.value!.currentTime = timestamp
|
|
39
35
|
|
|
40
36
|
const matchRoute = computed(() => !!$route && $route.no === $slidev?.nav.currentSlideNo)
|
|
41
|
-
const matchClick = computed(() => !!video.value && (
|
|
37
|
+
const matchClick = computed(() => !!video.value && (resolvedClickMap.get(video.value)?.isShown?.value ?? true))
|
|
42
38
|
const matchRouteAndClick = and(matchRoute, matchClick)
|
|
43
39
|
|
|
44
40
|
watch(matchRouteAndClick, () => {
|
package/builtin/VClicks.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { toArray } from '@antfu/utils'
|
|
8
8
|
import type { VNode, VNodeArrayChildren } from 'vue'
|
|
9
9
|
import { Comment, createVNode, defineComponent, h, isVNode, resolveDirective, withDirectives } from 'vue'
|
|
10
|
-
import {
|
|
10
|
+
import { normalizeAtValue } from '../composables/useClicks'
|
|
11
11
|
import VClickGap from './VClickGap.vue'
|
|
12
12
|
|
|
13
13
|
const listTags = ['ul', 'ol']
|
|
@@ -37,7 +37,12 @@ export default defineComponent({
|
|
|
37
37
|
},
|
|
38
38
|
render() {
|
|
39
39
|
const every = +this.every
|
|
40
|
-
const
|
|
40
|
+
const at = normalizeAtValue(this.at)
|
|
41
|
+
const isRelative = typeof at === 'string'
|
|
42
|
+
if (typeof at !== 'string' && typeof at !== 'number') {
|
|
43
|
+
console.warn('[slidev] Invalid at prop for v-clicks component:', at)
|
|
44
|
+
return
|
|
45
|
+
}
|
|
41
46
|
|
|
42
47
|
const click = resolveDirective('click')!
|
|
43
48
|
|
package/composables/useClicks.ts
CHANGED
|
@@ -1,67 +1,115 @@
|
|
|
1
1
|
import { clamp, sum } from '@antfu/utils'
|
|
2
|
-
import type { ClicksContext, SlideRoute } from '@slidev/types'
|
|
2
|
+
import type { ClicksContext, NormalizedAtValue, RawAtValue, SlideRoute } from '@slidev/types'
|
|
3
3
|
import type { Ref } from 'vue'
|
|
4
|
-
import { ref, shallowReactive } from 'vue'
|
|
5
|
-
import { normalizeAtProp } from '../logic/utils'
|
|
4
|
+
import { computed, ref, shallowReactive } from 'vue'
|
|
6
5
|
import { routeForceRefresh } from '../logic/route'
|
|
7
6
|
|
|
7
|
+
export function normalizeAtValue(at: RawAtValue): NormalizedAtValue {
|
|
8
|
+
if (at === false || at === 'false')
|
|
9
|
+
return null
|
|
10
|
+
if (at == null || at === true || at === 'true')
|
|
11
|
+
return '+1'
|
|
12
|
+
if (Array.isArray(at))
|
|
13
|
+
return [+at[0], +at[1]]
|
|
14
|
+
if (typeof at === 'string' && '+-'.includes(at[0]))
|
|
15
|
+
return at
|
|
16
|
+
return +at
|
|
17
|
+
}
|
|
18
|
+
|
|
8
19
|
export function createClicksContextBase(
|
|
9
20
|
current: Ref<number>,
|
|
10
21
|
clicksStart = 0,
|
|
11
22
|
clicksTotalOverrides?: number,
|
|
12
23
|
): ClicksContext {
|
|
13
|
-
const
|
|
14
|
-
const map: ClicksContext['map'] = shallowReactive(new Map())
|
|
15
|
-
|
|
16
|
-
return {
|
|
24
|
+
const context: ClicksContext = {
|
|
17
25
|
get current() {
|
|
18
26
|
// Here we haven't know clicksTotal yet.
|
|
19
|
-
return clamp(+current.value, clicksStart,
|
|
27
|
+
return clamp(+current.value, clicksStart, context.total)
|
|
20
28
|
},
|
|
21
29
|
set current(value) {
|
|
22
|
-
current.value = clamp(+value, clicksStart,
|
|
30
|
+
current.value = clamp(+value, clicksStart, context.total)
|
|
23
31
|
},
|
|
24
32
|
clicksStart,
|
|
25
|
-
relativeOffsets,
|
|
26
|
-
|
|
33
|
+
relativeOffsets: new Map(),
|
|
34
|
+
maxMap: shallowReactive(new Map()),
|
|
27
35
|
onMounted() { },
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (
|
|
31
|
-
const offset =
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
36
|
+
calculateSince(at, size = 1) {
|
|
37
|
+
let start: number, max: number, delta: number
|
|
38
|
+
if (typeof at === 'string') {
|
|
39
|
+
const offset = context.currentOffset
|
|
40
|
+
const value = +at
|
|
41
|
+
start = offset + value
|
|
42
|
+
max = offset + value + size - 1
|
|
43
|
+
delta = value + size - 1
|
|
37
44
|
}
|
|
38
45
|
else {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
46
|
+
start = at
|
|
47
|
+
max = at + size - 1
|
|
48
|
+
delta = 0
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
start,
|
|
52
|
+
end: +Number.POSITIVE_INFINITY,
|
|
53
|
+
max,
|
|
54
|
+
delta,
|
|
55
|
+
isCurrent: computed(() => context.current === start),
|
|
56
|
+
isActive: computed(() => context.current >= start),
|
|
44
57
|
}
|
|
45
58
|
},
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
59
|
+
calculateRange([a, b]) {
|
|
60
|
+
let start: number, end: number, delta: number
|
|
61
|
+
if (typeof a === 'string') {
|
|
62
|
+
const offset = context.currentOffset
|
|
63
|
+
start = offset + +a
|
|
64
|
+
delta = +a
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
start = a
|
|
68
|
+
delta = 0
|
|
69
|
+
}
|
|
70
|
+
if (typeof b === 'string') {
|
|
71
|
+
end = start + +b
|
|
72
|
+
delta += +b
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
end = b
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
start,
|
|
79
|
+
end,
|
|
80
|
+
max: end,
|
|
81
|
+
delta,
|
|
82
|
+
isCurrent: computed(() => context.current === start),
|
|
83
|
+
isActive: computed(() => start <= context.current && context.current < end),
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
calculate(at) {
|
|
87
|
+
if (at == null)
|
|
88
|
+
return null
|
|
89
|
+
if (Array.isArray(at))
|
|
90
|
+
return context.calculateRange(at)
|
|
91
|
+
return context.calculateSince(at)
|
|
92
|
+
},
|
|
93
|
+
register(el, { delta, max }) {
|
|
94
|
+
context.relativeOffsets.set(el, delta)
|
|
95
|
+
context.maxMap.set(el, max)
|
|
49
96
|
},
|
|
50
97
|
unregister(el) {
|
|
51
|
-
relativeOffsets.delete(el)
|
|
52
|
-
|
|
98
|
+
context.relativeOffsets.delete(el)
|
|
99
|
+
context.maxMap.delete(el)
|
|
53
100
|
},
|
|
54
101
|
get currentOffset() {
|
|
55
102
|
// eslint-disable-next-line no-unused-expressions
|
|
56
103
|
routeForceRefresh.value
|
|
57
|
-
return sum(...relativeOffsets.values())
|
|
104
|
+
return sum(...context.relativeOffsets.values())
|
|
58
105
|
},
|
|
59
106
|
get total() {
|
|
60
107
|
// eslint-disable-next-line no-unused-expressions
|
|
61
108
|
routeForceRefresh.value
|
|
62
|
-
return clicksTotalOverrides ?? Math.max(0, ...
|
|
109
|
+
return clicksTotalOverrides ?? Math.max(0, ...context.maxMap.values())
|
|
63
110
|
},
|
|
64
111
|
}
|
|
112
|
+
return context
|
|
65
113
|
}
|
|
66
114
|
|
|
67
115
|
export function createFixedClicks(
|
|
@@ -52,7 +52,9 @@ export function useDragElementsUpdater(no: number) {
|
|
|
52
52
|
return
|
|
53
53
|
frontmatter.dragPos[id] = posStr
|
|
54
54
|
newPatch = {
|
|
55
|
-
frontmatter
|
|
55
|
+
frontmatter: {
|
|
56
|
+
dragPos: frontmatter.dragPos,
|
|
57
|
+
},
|
|
56
58
|
}
|
|
57
59
|
}
|
|
58
60
|
else {
|
|
@@ -267,8 +269,10 @@ export function useDragElement(directive: DirectiveBinding | null, posRaw?: stri
|
|
|
267
269
|
|
|
268
270
|
watchStopHandles.push(
|
|
269
271
|
onClickOutside(container, (ev) => {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
+
const container = document.querySelector('#drag-control-container')
|
|
273
|
+
if (container && ev.target && container.contains(ev.target as HTMLElement))
|
|
274
|
+
return
|
|
275
|
+
state.stopDragging()
|
|
272
276
|
}),
|
|
273
277
|
watch(useWindowFocus(), (focused) => {
|
|
274
278
|
if (!focused)
|
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
|
|
package/constants.ts
CHANGED
|
@@ -14,7 +14,6 @@ export const injectionRenderContext = '$$slidev-render-context' as unknown as In
|
|
|
14
14
|
export const injectionActive = '$$slidev-active' as unknown as InjectionKey<Ref<boolean>>
|
|
15
15
|
export const injectionFrontmatter = '$$slidev-fontmatter' as unknown as InjectionKey<Record<string, any>>
|
|
16
16
|
export const injectionSlideZoom = '$$slidev-slide-zoom' as unknown as InjectionKey<ComputedRef<number>>
|
|
17
|
-
export const injectionClickVisibility = '$$slidev-click-visibility' as unknown as InjectionKey<ComputedRef<true | 'before' | 'after'>>
|
|
18
17
|
|
|
19
18
|
export const CLASS_VCLICK_TARGET = 'slidev-vclick-target'
|
|
20
19
|
export const CLASS_VCLICK_HIDDEN = 'slidev-vclick-hidden'
|
|
@@ -77,4 +76,5 @@ export const HEADMATTER_FIELDS = [
|
|
|
77
76
|
'drawings',
|
|
78
77
|
'htmlAttrs',
|
|
79
78
|
'mdc',
|
|
79
|
+
'contextMenu',
|
|
80
80
|
]
|
package/env.ts
CHANGED
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>
|
|
@@ -5,7 +5,6 @@ import { downloadPDF } from '../utils'
|
|
|
5
5
|
import { activeElement, breakpoints, fullscreen, presenterLayout, showEditor, showInfoDialog, showPresenterCursor, toggleOverview, togglePresenterLayout } from '../state'
|
|
6
6
|
import { configs } from '../env'
|
|
7
7
|
import { useNav } from '../composables/useNav'
|
|
8
|
-
import { getSlidePath } from '../logic/slides'
|
|
9
8
|
import { useDrawings } from '../composables/useDrawings'
|
|
10
9
|
import Settings from './Settings.vue'
|
|
11
10
|
import MenuButton from './MenuButton.vue'
|
|
@@ -21,7 +20,6 @@ const props = defineProps({
|
|
|
21
20
|
})
|
|
22
21
|
|
|
23
22
|
const {
|
|
24
|
-
currentRoute,
|
|
25
23
|
currentSlideNo,
|
|
26
24
|
hasNext,
|
|
27
25
|
hasPrev,
|
|
@@ -31,6 +29,8 @@ const {
|
|
|
31
29
|
next,
|
|
32
30
|
prev,
|
|
33
31
|
total,
|
|
32
|
+
enterPresenter,
|
|
33
|
+
exitPresenter,
|
|
34
34
|
} = useNav()
|
|
35
35
|
const {
|
|
36
36
|
brush,
|
|
@@ -40,11 +40,6 @@ const {
|
|
|
40
40
|
const md = breakpoints.smaller('md')
|
|
41
41
|
const { isFullscreen, toggle: toggleFullscreen } = fullscreen
|
|
42
42
|
|
|
43
|
-
const presenterPassword = computed(() => currentRoute.value.query.password)
|
|
44
|
-
const query = computed(() => presenterPassword.value ? `?password=${presenterPassword.value}` : '')
|
|
45
|
-
const presenterLink = computed(() => `${getSlidePath(currentSlideNo.value, true)}${query.value}`)
|
|
46
|
-
const nonPresenterLink = computed(() => `${getSlidePath(currentSlideNo.value, false)}${query.value}`)
|
|
47
|
-
|
|
48
43
|
const root = ref<HTMLDivElement>()
|
|
49
44
|
function onMouseLeave() {
|
|
50
45
|
if (root.value && activeElement.value && root.value.contains(activeElement.value))
|
|
@@ -124,12 +119,12 @@ if (__SLIDEV_FEATURE_DRAWINGS__)
|
|
|
124
119
|
</template>
|
|
125
120
|
|
|
126
121
|
<template v-if="!isEmbedded">
|
|
127
|
-
<
|
|
122
|
+
<IconButton v-if="isPresenter" title="Play Mode" @click="exitPresenter">
|
|
128
123
|
<carbon:presentation-file />
|
|
129
|
-
</
|
|
130
|
-
<
|
|
124
|
+
</IconButton>
|
|
125
|
+
<IconButton v-if="__SLIDEV_FEATURE_PRESENTER__ && isPresenterAvailable" title="Presenter Mode" @click="enterPresenter">
|
|
131
126
|
<carbon:user-speaker />
|
|
132
|
-
</
|
|
127
|
+
</IconButton>
|
|
133
128
|
|
|
134
129
|
<IconButton
|
|
135
130
|
v-if="__DEV__ && __SLIDEV_FEATURE_EDITOR__"
|