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