@slidev/client 0.48.0-beta.9 → 0.48.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/App.vue +7 -0
- package/builtin/Arrow.vue +2 -4
- package/builtin/CodeBlockWrapper.vue +33 -28
- package/builtin/KaTexBlockWrapper.vue +1 -1
- package/builtin/Link.vue +3 -1
- package/builtin/Mermaid.vue +4 -3
- package/builtin/Monaco.vue +166 -93
- package/builtin/ShikiMagicMove.vue +103 -0
- package/builtin/SlidevVideo.vue +1 -1
- package/builtin/Toc.vue +1 -1
- package/builtin/TocList.vue +4 -3
- package/builtin/Tweet.vue +12 -9
- package/builtin/VClick.ts +2 -1
- package/composables/useClicks.ts +19 -32
- package/composables/useDarkMode.ts +9 -0
- package/composables/useDrawings.ts +181 -0
- package/composables/useNav.ts +346 -44
- package/{logic/note.ts → composables/useSlideInfo.ts} +13 -16
- package/composables/useSwipeControls.ts +43 -0
- package/composables/useTocTree.ts +81 -0
- package/composables/useViewTransition.ts +7 -4
- package/constants.ts +4 -3
- package/context.ts +13 -6
- package/env.ts +7 -16
- package/index.html +1 -0
- package/index.ts +12 -0
- package/internals/ClicksSlider.vue +97 -0
- package/internals/CodeRunner.vue +142 -0
- package/internals/Controls.vue +2 -2
- package/internals/DomElement.vue +18 -0
- package/internals/DrawingControls.vue +14 -15
- package/internals/DrawingLayer.vue +6 -5
- package/internals/DrawingPreview.vue +4 -2
- package/internals/Goto.vue +9 -6
- package/internals/IconButton.vue +3 -2
- package/internals/NavControls.vue +30 -11
- package/internals/NoteDisplay.vue +131 -8
- package/internals/NoteEditable.vue +129 -0
- package/internals/NoteStatic.vue +5 -2
- package/internals/PrintContainer.vue +11 -8
- package/internals/PrintSlide.vue +11 -12
- package/internals/PrintSlideClick.vue +14 -19
- package/internals/{SlidesOverview.vue → QuickOverview.vue} +27 -24
- package/internals/RecordingControls.vue +1 -1
- package/internals/RecordingDialog.vue +3 -3
- package/internals/{Editor.vue → SideEditor.vue} +24 -15
- package/internals/SlideContainer.vue +13 -9
- package/internals/SlideLoading.vue +19 -0
- package/internals/SlideWrapper.vue +79 -0
- package/internals/SlidesShow.vue +36 -22
- package/layouts/error.vue +5 -0
- package/layouts/two-cols-header.vue +9 -3
- package/logic/overview.ts +2 -2
- package/logic/route.ts +16 -5
- package/logic/slides.ts +20 -0
- package/logic/transition.ts +50 -0
- package/logic/utils.ts +24 -1
- package/main.ts +3 -15
- package/{setup → modules}/codemirror.ts +1 -3
- package/modules/context.ts +1 -46
- package/modules/mermaid.ts +9 -8
- package/package.json +21 -15
- package/pages/notes.vue +6 -3
- package/pages/overview.vue +139 -51
- package/pages/play.vue +16 -9
- package/pages/presenter/print.vue +10 -5
- package/pages/presenter.vue +122 -104
- package/pages/print.vue +4 -3
- package/routes.ts +8 -54
- package/setup/code-runners.ts +164 -0
- package/setup/main.ts +39 -9
- package/setup/mermaid.ts +5 -6
- package/setup/monaco.ts +114 -51
- package/setup/root.ts +62 -18
- package/setup/shortcuts.ts +15 -12
- package/shim-vue.d.ts +34 -0
- package/shim.d.ts +1 -13
- package/state/index.ts +2 -2
- package/styles/code.css +9 -5
- package/styles/index.css +63 -7
- package/styles/katex.css +1 -1
- package/styles/layouts-base.css +11 -8
- package/styles/shiki-twoslash.css +1 -1
- package/styles/vars.css +1 -1
- package/uno.config.ts +10 -1
- package/utils.ts +15 -2
- package/composables/useContext.ts +0 -17
- package/composables/useTweetScript.ts +0 -17
- package/iframes/monaco/index.css +0 -28
- package/iframes/monaco/index.html +0 -7
- package/iframes/monaco/index.ts +0 -260
- package/internals/NoteEditor.vue +0 -92
- package/internals/SlideWrapper.ts +0 -58
- package/logic/drawings.ts +0 -161
- package/logic/nav.ts +0 -278
- package/setup/prettier.ts +0 -43
- /package/{composables → logic}/hmr.ts +0 -0
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { Menu } from 'floating-vue'
|
|
3
|
-
import '
|
|
4
|
-
import
|
|
3
|
+
import { useDrawings } from '../composables/useDrawings'
|
|
4
|
+
import VerticalDivider from './VerticalDivider.vue'
|
|
5
|
+
import Draggable from './Draggable.vue'
|
|
6
|
+
import IconButton from './IconButton.vue'
|
|
7
|
+
|
|
8
|
+
const {
|
|
5
9
|
brush,
|
|
6
|
-
brushColors,
|
|
7
10
|
canClear,
|
|
8
11
|
canRedo,
|
|
9
12
|
canUndo,
|
|
10
|
-
|
|
13
|
+
clear,
|
|
11
14
|
drauu,
|
|
12
15
|
drawingEnabled,
|
|
13
16
|
drawingMode,
|
|
14
17
|
drawingPinned,
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
import Draggable from './Draggable.vue'
|
|
18
|
-
import IconButton from './IconButton.vue'
|
|
18
|
+
brushColors,
|
|
19
|
+
} = useDrawings()
|
|
19
20
|
|
|
20
21
|
function undo() {
|
|
21
22
|
drauu.undo()
|
|
@@ -110,7 +111,7 @@ function setBrushColor(color: typeof brush.color) {
|
|
|
110
111
|
<IconButton title="Redo" :class="{ disabled: !canRedo }" @click="redo()">
|
|
111
112
|
<carbon:redo />
|
|
112
113
|
</IconButton>
|
|
113
|
-
<IconButton title="Delete" :class="{ disabled: !canClear }" @click="
|
|
114
|
+
<IconButton title="Delete" :class="{ disabled: !canClear }" @click="clear()">
|
|
114
115
|
<carbon:trash-can />
|
|
115
116
|
</IconButton>
|
|
116
117
|
|
|
@@ -131,10 +132,8 @@ function setBrushColor(color: typeof brush.color) {
|
|
|
131
132
|
</Draggable>
|
|
132
133
|
</template>
|
|
133
134
|
|
|
134
|
-
<style
|
|
135
|
-
.v-popper--theme-menu {
|
|
136
|
-
|
|
137
|
-
@apply border-main;
|
|
138
|
-
}
|
|
135
|
+
<style>
|
|
136
|
+
.v-popper--theme-menu .v-popper__arrow-inner {
|
|
137
|
+
--uno: border-main;
|
|
139
138
|
}
|
|
140
|
-
</style
|
|
139
|
+
</style>../composables/drawings
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { injectionSlideScale } from '../constants'
|
|
3
|
+
import { useSlideContext } from '../context'
|
|
4
|
+
import { useDrawings } from '../composables/useDrawings'
|
|
6
5
|
|
|
7
|
-
const
|
|
6
|
+
const { drauu, drawingEnabled, loadCanvas } = useDrawings()
|
|
7
|
+
|
|
8
|
+
const scale = useSlideContext().$scale
|
|
8
9
|
const svg = ref<SVGSVGElement>()
|
|
9
10
|
|
|
10
11
|
onMounted(() => {
|
|
@@ -24,4 +25,4 @@ onBeforeUnmount(() => {
|
|
|
24
25
|
class="w-full h-full absolute top-0"
|
|
25
26
|
:class="{ 'pointer-events-none': !drawingEnabled, 'touch-none': drawingEnabled }"
|
|
26
27
|
/>
|
|
27
|
-
</template
|
|
28
|
+
</template>../composables/drawings
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import {
|
|
2
|
+
import { useDrawings } from '../composables/useDrawings'
|
|
3
3
|
|
|
4
4
|
defineProps<{ page: number }>()
|
|
5
|
+
|
|
6
|
+
const { drawingState } = useDrawings()
|
|
5
7
|
</script>
|
|
6
8
|
|
|
7
9
|
<template>
|
|
@@ -10,4 +12,4 @@ defineProps<{ page: number }>()
|
|
|
10
12
|
class="w-full h-full absolute top-0 pointer-events-none"
|
|
11
13
|
v-html="drawingState[page]"
|
|
12
14
|
/>
|
|
13
|
-
</template
|
|
15
|
+
</template>../composables/drawings
|
package/internals/Goto.vue
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed, ref, watch } from 'vue'
|
|
3
3
|
import Fuse from 'fuse.js'
|
|
4
|
-
import { go, rawRoutes } from '../logic/nav'
|
|
5
4
|
import { activeElement, showGotoDialog } from '../state'
|
|
6
|
-
import
|
|
5
|
+
import { useNav } from '../composables/useNav'
|
|
6
|
+
import Titles from '#slidev/title-renderer'
|
|
7
7
|
|
|
8
8
|
const container = ref<HTMLDivElement>()
|
|
9
9
|
const input = ref<HTMLInputElement>()
|
|
@@ -12,11 +12,13 @@ const items = ref<HTMLLIElement[]>()
|
|
|
12
12
|
const text = ref('')
|
|
13
13
|
const selectedIndex = ref(0)
|
|
14
14
|
|
|
15
|
+
const { go, slides } = useNav()
|
|
16
|
+
|
|
15
17
|
function notNull<T>(value: T | null | undefined): value is T {
|
|
16
18
|
return value !== null && value !== undefined
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
const fuse = computed(() => new Fuse(
|
|
21
|
+
const fuse = computed(() => new Fuse(slides.value.map(i => i.meta?.slide).filter(notNull), {
|
|
20
22
|
keys: ['no', 'title'],
|
|
21
23
|
threshold: 0.3,
|
|
22
24
|
shouldSort: true,
|
|
@@ -165,10 +167,11 @@ watch(activeElement, () => {
|
|
|
165
167
|
</div>
|
|
166
168
|
</template>
|
|
167
169
|
|
|
168
|
-
<style scoped
|
|
170
|
+
<style scoped>
|
|
169
171
|
.autocomplete-list {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
+
--uno: bg-main mt-1;
|
|
173
|
+
overflow: auto;
|
|
174
|
+
max-height: calc(100vh - 100px);
|
|
172
175
|
}
|
|
173
176
|
|
|
174
177
|
.autocomplete {
|
package/internals/IconButton.vue
CHANGED
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
defineProps<{
|
|
3
3
|
title: string
|
|
4
4
|
icon?: string
|
|
5
|
+
as?: string
|
|
5
6
|
}>()
|
|
6
7
|
</script>
|
|
7
8
|
|
|
8
9
|
<template>
|
|
9
|
-
<button class="slidev-icon-btn" :title="title"
|
|
10
|
+
<component :is="as || 'button'" class="slidev-icon-btn" :title="title">
|
|
10
11
|
<span class="sr-only">{{ title }}</span>
|
|
11
12
|
<slot>
|
|
12
13
|
<div :class="icon" />
|
|
13
14
|
</slot>
|
|
14
|
-
</
|
|
15
|
+
</component>
|
|
15
16
|
</template>
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed, ref, shallowRef } from 'vue'
|
|
3
3
|
import { isColorSchemaConfigured, isDark, toggleDark } from '../logic/dark'
|
|
4
|
-
import {
|
|
4
|
+
import { downloadPDF } from '../utils'
|
|
5
5
|
import { activeElement, breakpoints, fullscreen, presenterLayout, showEditor, showInfoDialog, showPresenterCursor, toggleOverview, togglePresenterLayout } from '../state'
|
|
6
|
-
import { brush, drawingEnabled } from '../logic/drawings'
|
|
7
6
|
import { configs } from '../env'
|
|
7
|
+
import { useNav } from '../composables/useNav'
|
|
8
|
+
import { getSlidePath } from '../logic/slides'
|
|
9
|
+
import { useDrawings } from '../composables/useDrawings'
|
|
8
10
|
import Settings from './Settings.vue'
|
|
9
11
|
import MenuButton from './MenuButton.vue'
|
|
10
12
|
import VerticalDivider from './VerticalDivider.vue'
|
|
11
13
|
import IconButton from './IconButton.vue'
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
import CustomNavControls from '/@slidev/custom-nav-controls'
|
|
15
|
+
import CustomNavControls from '#slidev/custom-nav-controls'
|
|
15
16
|
|
|
16
17
|
const props = defineProps({
|
|
17
18
|
persist: {
|
|
@@ -19,12 +20,30 @@ const props = defineProps({
|
|
|
19
20
|
},
|
|
20
21
|
})
|
|
21
22
|
|
|
23
|
+
const {
|
|
24
|
+
currentRoute,
|
|
25
|
+
currentSlideNo,
|
|
26
|
+
hasNext,
|
|
27
|
+
hasPrev,
|
|
28
|
+
isEmbedded,
|
|
29
|
+
isPresenter,
|
|
30
|
+
isPresenterAvailable,
|
|
31
|
+
next,
|
|
32
|
+
prev,
|
|
33
|
+
total,
|
|
34
|
+
} = useNav()
|
|
35
|
+
const {
|
|
36
|
+
brush,
|
|
37
|
+
drawingEnabled,
|
|
38
|
+
} = useDrawings()
|
|
39
|
+
|
|
22
40
|
const md = breakpoints.smaller('md')
|
|
23
41
|
const { isFullscreen, toggle: toggleFullscreen } = fullscreen
|
|
24
42
|
|
|
43
|
+
const presenterPassword = computed(() => currentRoute.value.query.password)
|
|
25
44
|
const query = computed(() => presenterPassword.value ? `?password=${presenterPassword.value}` : '')
|
|
26
|
-
const presenterLink = computed(() =>
|
|
27
|
-
const nonPresenterLink = computed(() =>
|
|
45
|
+
const presenterLink = computed(() => `${getSlidePath(currentSlideNo.value, true)}${query.value}`)
|
|
46
|
+
const nonPresenterLink = computed(() => `${getSlidePath(currentSlideNo.value, false)}${query.value}`)
|
|
28
47
|
|
|
29
48
|
const root = ref<HTMLDivElement>()
|
|
30
49
|
function onMouseLeave() {
|
|
@@ -108,20 +127,20 @@ if (__SLIDEV_FEATURE_DRAWINGS__)
|
|
|
108
127
|
<RouterLink v-if="isPresenter" :to="nonPresenterLink" class="slidev-icon-btn" title="Play Mode">
|
|
109
128
|
<carbon:presentation-file />
|
|
110
129
|
</RouterLink>
|
|
111
|
-
<RouterLink v-if="__SLIDEV_FEATURE_PRESENTER__ &&
|
|
130
|
+
<RouterLink v-if="__SLIDEV_FEATURE_PRESENTER__ && isPresenterAvailable" :to="presenterLink" class="slidev-icon-btn" title="Presenter Mode">
|
|
112
131
|
<carbon:user-speaker />
|
|
113
132
|
</RouterLink>
|
|
114
133
|
|
|
115
134
|
<IconButton
|
|
116
135
|
v-if="__DEV__ && __SLIDEV_FEATURE_EDITOR__"
|
|
117
136
|
:title="showEditor ? 'Hide editor' : 'Show editor'"
|
|
118
|
-
class="
|
|
137
|
+
class="lt-md:hidden"
|
|
119
138
|
@click="showEditor = !showEditor"
|
|
120
139
|
>
|
|
121
140
|
<carbon:text-annotation-toggle />
|
|
122
141
|
</IconButton>
|
|
123
142
|
|
|
124
|
-
<IconButton v-if="isPresenter" title="Toggle Presenter Layout" @click="togglePresenterLayout">
|
|
143
|
+
<IconButton v-if="isPresenter" title="Toggle Presenter Layout" class="aspect-ratio-initial" @click="togglePresenterLayout">
|
|
125
144
|
<carbon:template />
|
|
126
145
|
{{ presenterLayout }}
|
|
127
146
|
</IconButton>
|
|
@@ -157,7 +176,7 @@ if (__SLIDEV_FEATURE_DRAWINGS__)
|
|
|
157
176
|
|
|
158
177
|
<div class="h-40px flex" p="l-1 t-0.5 r-2" text="sm leading-2">
|
|
159
178
|
<div class="my-auto">
|
|
160
|
-
{{
|
|
179
|
+
{{ currentSlideNo }}
|
|
161
180
|
<span class="opacity-50">/ {{ total }}</span>
|
|
162
181
|
</div>
|
|
163
182
|
</div>
|
|
@@ -165,4 +184,4 @@ if (__SLIDEV_FEATURE_DRAWINGS__)
|
|
|
165
184
|
<CustomNavControls />
|
|
166
185
|
</div>
|
|
167
186
|
</nav>
|
|
168
|
-
</template
|
|
187
|
+
</template>../composables/drawings
|
|
@@ -1,36 +1,159 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
|
3
|
+
import type { ClicksContext } from '@slidev/types'
|
|
4
|
+
import { CLICKS_MAX } from '../constants'
|
|
5
|
+
|
|
2
6
|
const props = defineProps<{
|
|
3
7
|
class?: string
|
|
4
8
|
noteHtml?: string
|
|
5
9
|
note?: string
|
|
6
10
|
placeholder?: string
|
|
11
|
+
clicksContext?: ClicksContext
|
|
12
|
+
autoScroll?: boolean
|
|
13
|
+
}>()
|
|
14
|
+
|
|
15
|
+
const emit = defineEmits<{
|
|
16
|
+
(type: 'markerDblclick', e: MouseEvent, clicks: number): void
|
|
17
|
+
(type: 'markerClick', e: MouseEvent, clicks: number): void
|
|
7
18
|
}>()
|
|
8
19
|
|
|
9
|
-
|
|
20
|
+
const withClicks = computed(() => props.clicksContext?.current != null && props.noteHtml?.includes('slidev-note-click-mark'))
|
|
21
|
+
const noteDisplay = ref<HTMLElement | null>(null)
|
|
22
|
+
|
|
23
|
+
const CLASS_FADE = 'slidev-note-fade'
|
|
24
|
+
const CLASS_MARKER = 'slidev-note-click-mark'
|
|
25
|
+
|
|
26
|
+
function highlightNote() {
|
|
27
|
+
if (!noteDisplay.value || !withClicks.value)
|
|
28
|
+
return
|
|
29
|
+
|
|
30
|
+
const markers = Array.from(noteDisplay.value.querySelectorAll(`.${CLASS_MARKER}`)) as HTMLElement[]
|
|
31
|
+
|
|
32
|
+
const current = +(props.clicksContext?.current ?? CLICKS_MAX)
|
|
33
|
+
const disabled = current < 0 || current >= CLICKS_MAX
|
|
34
|
+
|
|
35
|
+
const nodeToIgnores = new Set<Element>()
|
|
36
|
+
function ignoreParent(node: Element) {
|
|
37
|
+
if (!node || node === noteDisplay.value)
|
|
38
|
+
return
|
|
39
|
+
nodeToIgnores.add(node)
|
|
40
|
+
if (node.parentElement)
|
|
41
|
+
ignoreParent(node.parentElement)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const markersMap = new Map<number, HTMLElement>()
|
|
45
|
+
|
|
46
|
+
// Convert all sibling text nodes to spans, so we attach classes to them
|
|
47
|
+
for (const marker of markers) {
|
|
48
|
+
const parent = marker.parentElement!
|
|
49
|
+
const clicks = Number(marker.dataset!.clicks)
|
|
50
|
+
markersMap.set(clicks, marker)
|
|
51
|
+
// Ignore the parents of the marker, so the class only applies to the children
|
|
52
|
+
ignoreParent(parent)
|
|
53
|
+
Array.from(parent!.childNodes)
|
|
54
|
+
.forEach((node) => {
|
|
55
|
+
if (node.nodeType === 3) { // text node
|
|
56
|
+
const span = document.createElement('span')
|
|
57
|
+
span.textContent = node.textContent
|
|
58
|
+
parent.insertBefore(span, node)
|
|
59
|
+
node.remove()
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
const children = Array.from(noteDisplay.value.querySelectorAll('*'))
|
|
64
|
+
|
|
65
|
+
let count = 0
|
|
66
|
+
|
|
67
|
+
// Segmenting notes by clicks
|
|
68
|
+
const segments = new Map<number, Element[]>()
|
|
69
|
+
for (const child of children) {
|
|
70
|
+
if (!segments.has(count))
|
|
71
|
+
segments.set(count, [])
|
|
72
|
+
segments.get(count)!.push(child)
|
|
73
|
+
// Update count when reach marker
|
|
74
|
+
if (child.classList.contains(CLASS_MARKER))
|
|
75
|
+
count = Number((child as HTMLElement).dataset.clicks) || (count + 1)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Apply
|
|
79
|
+
for (const [count, els] of segments) {
|
|
80
|
+
if (disabled) {
|
|
81
|
+
els.forEach(el => el.classList.remove(CLASS_FADE))
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
els.forEach(el => el.classList.toggle(
|
|
85
|
+
CLASS_FADE,
|
|
86
|
+
nodeToIgnores.has(el)
|
|
87
|
+
? false
|
|
88
|
+
: count !== current,
|
|
89
|
+
))
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
for (const [clicks, marker] of markersMap) {
|
|
94
|
+
marker.classList.remove(CLASS_FADE)
|
|
95
|
+
marker.classList.toggle(`${CLASS_MARKER}-past`, disabled ? false : clicks < current)
|
|
96
|
+
marker.classList.toggle(`${CLASS_MARKER}-active`, disabled ? false : clicks === current)
|
|
97
|
+
marker.classList.toggle(`${CLASS_MARKER}-next`, disabled ? false : clicks === current + 1)
|
|
98
|
+
marker.classList.toggle(`${CLASS_MARKER}-future`, disabled ? false : clicks > current + 1)
|
|
99
|
+
marker.ondblclick = (e) => {
|
|
100
|
+
emit('markerDblclick', e, clicks)
|
|
101
|
+
if (e.defaultPrevented)
|
|
102
|
+
return
|
|
103
|
+
props.clicksContext!.current = clicks
|
|
104
|
+
e.stopPropagation()
|
|
105
|
+
e.stopImmediatePropagation()
|
|
106
|
+
}
|
|
107
|
+
marker.onclick = (e) => {
|
|
108
|
+
emit('markerClick', e, clicks)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (props.autoScroll && clicks === current)
|
|
112
|
+
marker.scrollIntoView({ block: 'center', behavior: 'smooth' })
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
watch(
|
|
117
|
+
() => [props.noteHtml, props.clicksContext?.current],
|
|
118
|
+
() => {
|
|
119
|
+
nextTick(() => {
|
|
120
|
+
highlightNote()
|
|
121
|
+
})
|
|
122
|
+
},
|
|
123
|
+
{ immediate: true },
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
onMounted(() => {
|
|
127
|
+
highlightNote()
|
|
128
|
+
})
|
|
10
129
|
</script>
|
|
11
130
|
|
|
12
131
|
<template>
|
|
13
132
|
<div
|
|
14
133
|
v-if="noteHtml"
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
134
|
+
ref="noteDisplay"
|
|
135
|
+
class="prose overflow-auto outline-none slidev-note"
|
|
136
|
+
:class="[props.class, withClicks ? 'slidev-note-with-clicks' : '']"
|
|
18
137
|
v-html="noteHtml"
|
|
19
138
|
/>
|
|
20
139
|
<div
|
|
21
140
|
v-else-if="note"
|
|
22
|
-
class="prose overflow-auto outline-none"
|
|
141
|
+
class="prose overflow-auto outline-none slidev-note"
|
|
23
142
|
:class="props.class"
|
|
24
|
-
@click="$emit('click')"
|
|
25
143
|
>
|
|
26
144
|
<p v-text="note" />
|
|
27
145
|
</div>
|
|
28
146
|
<div
|
|
29
147
|
v-else
|
|
30
|
-
class="prose overflow-auto outline-none opacity-50 italic select-none"
|
|
148
|
+
class="prose overflow-auto outline-none opacity-50 italic select-none slidev-note"
|
|
31
149
|
:class="props.class"
|
|
32
|
-
@click="$emit('click')"
|
|
33
150
|
>
|
|
34
151
|
<p v-text="props.placeholder || 'No notes.'" />
|
|
35
152
|
</div>
|
|
36
153
|
</template>
|
|
154
|
+
|
|
155
|
+
<style>
|
|
156
|
+
.slidev-note :first-child {
|
|
157
|
+
margin-top: 0;
|
|
158
|
+
}
|
|
159
|
+
</style>
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { PropType } from 'vue'
|
|
3
|
+
import { nextTick, ref, toRef, watch, watchEffect } from 'vue'
|
|
4
|
+
import { ignorableWatch, onClickOutside, useVModel } from '@vueuse/core'
|
|
5
|
+
import type { ClicksContext } from '@slidev/types'
|
|
6
|
+
import { useDynamicSlideInfo } from '../composables/useSlideInfo'
|
|
7
|
+
import NoteDisplay from './NoteDisplay.vue'
|
|
8
|
+
|
|
9
|
+
const props = defineProps({
|
|
10
|
+
no: {
|
|
11
|
+
type: Number,
|
|
12
|
+
required: true,
|
|
13
|
+
},
|
|
14
|
+
class: {
|
|
15
|
+
default: '',
|
|
16
|
+
},
|
|
17
|
+
editing: {
|
|
18
|
+
default: false,
|
|
19
|
+
},
|
|
20
|
+
style: {
|
|
21
|
+
default: () => ({}),
|
|
22
|
+
},
|
|
23
|
+
placeholder: {
|
|
24
|
+
default: 'No notes for this slide',
|
|
25
|
+
},
|
|
26
|
+
clicksContext: {
|
|
27
|
+
type: Object as PropType<ClicksContext>,
|
|
28
|
+
},
|
|
29
|
+
autoHeight: {
|
|
30
|
+
default: false,
|
|
31
|
+
},
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const emit = defineEmits<{
|
|
35
|
+
(type: 'update:editing', value: boolean): void
|
|
36
|
+
(type: 'markerDblclick', e: MouseEvent, clicks: number): void
|
|
37
|
+
(type: 'markerClick', e: MouseEvent, clicks: number): void
|
|
38
|
+
}>()
|
|
39
|
+
|
|
40
|
+
const editing = useVModel(props, 'editing', emit, { passive: true })
|
|
41
|
+
|
|
42
|
+
const { info, update } = useDynamicSlideInfo(toRef(props, 'no'))
|
|
43
|
+
|
|
44
|
+
const note = ref('')
|
|
45
|
+
let timer: any
|
|
46
|
+
|
|
47
|
+
// Send back the note on changes
|
|
48
|
+
const { ignoreUpdates } = ignorableWatch(
|
|
49
|
+
note,
|
|
50
|
+
(v) => {
|
|
51
|
+
if (!editing.value)
|
|
52
|
+
return
|
|
53
|
+
const id = props.no
|
|
54
|
+
clearTimeout(timer)
|
|
55
|
+
timer = setTimeout(() => {
|
|
56
|
+
update({ note: v }, id)
|
|
57
|
+
}, 500)
|
|
58
|
+
},
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
// Update note value when info changes
|
|
62
|
+
watch(
|
|
63
|
+
() => info.value?.note,
|
|
64
|
+
(value = '') => {
|
|
65
|
+
if (editing.value)
|
|
66
|
+
return
|
|
67
|
+
clearTimeout(timer)
|
|
68
|
+
ignoreUpdates(() => {
|
|
69
|
+
note.value = value
|
|
70
|
+
})
|
|
71
|
+
},
|
|
72
|
+
{ immediate: true, flush: 'sync' },
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
const inputEl = ref<HTMLTextAreaElement>()
|
|
76
|
+
const inputHeight = ref<number | null>()
|
|
77
|
+
|
|
78
|
+
watchEffect(() => {
|
|
79
|
+
if (editing.value)
|
|
80
|
+
inputEl.value?.focus()
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
onClickOutside(inputEl, () => {
|
|
84
|
+
editing.value = false
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
function calculateEditorHeight() {
|
|
88
|
+
if (!props.autoHeight || !inputEl.value || !editing.value)
|
|
89
|
+
return
|
|
90
|
+
if (inputEl.value.scrollHeight > inputEl.value.clientHeight)
|
|
91
|
+
inputEl.value.style.height = `${inputEl.value.scrollHeight}px`
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
watch(
|
|
95
|
+
[note, editing],
|
|
96
|
+
() => {
|
|
97
|
+
nextTick(() => {
|
|
98
|
+
calculateEditorHeight()
|
|
99
|
+
})
|
|
100
|
+
},
|
|
101
|
+
{ flush: 'post', immediate: true },
|
|
102
|
+
)
|
|
103
|
+
</script>
|
|
104
|
+
|
|
105
|
+
<template>
|
|
106
|
+
<NoteDisplay
|
|
107
|
+
v-if="!editing"
|
|
108
|
+
class="border-transparent border-2"
|
|
109
|
+
:class="[props.class, note ? '' : 'opacity-25 italic select-none']"
|
|
110
|
+
:style="props.style"
|
|
111
|
+
:note="note || placeholder"
|
|
112
|
+
:note-html="info?.noteHTML"
|
|
113
|
+
:clicks-context="clicksContext"
|
|
114
|
+
:auto-scroll="!autoHeight"
|
|
115
|
+
@marker-click="(e, clicks) => emit('markerClick', e, clicks)"
|
|
116
|
+
@marker-dblclick="(e, clicks) => emit('markerDblclick', e, clicks)"
|
|
117
|
+
/>
|
|
118
|
+
<textarea
|
|
119
|
+
v-else
|
|
120
|
+
ref="inputEl"
|
|
121
|
+
v-model="note"
|
|
122
|
+
class="prose resize-none overflow-auto outline-none bg-transparent block border-primary border-2"
|
|
123
|
+
style="line-height: 1.75;"
|
|
124
|
+
:style="[props.style, inputHeight != null ? { height: `${inputHeight}px` } : {}]"
|
|
125
|
+
:class="props.class"
|
|
126
|
+
:placeholder="placeholder"
|
|
127
|
+
@keydown.esc="editing = false"
|
|
128
|
+
/>
|
|
129
|
+
</template>
|
package/internals/NoteStatic.vue
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import {
|
|
2
|
+
import type { ClicksContext } from '@slidev/types'
|
|
3
|
+
import { useSlideInfo } from '../composables/useSlideInfo'
|
|
3
4
|
import NoteDisplay from './NoteDisplay.vue'
|
|
4
5
|
|
|
5
6
|
const props = defineProps<{
|
|
6
|
-
no
|
|
7
|
+
no: number
|
|
7
8
|
class?: string
|
|
9
|
+
clicksContext?: ClicksContext
|
|
8
10
|
}>()
|
|
9
11
|
|
|
10
12
|
const { info } = useSlideInfo(props.no)
|
|
@@ -15,5 +17,6 @@ const { info } = useSlideInfo(props.no)
|
|
|
15
17
|
:class="props.class"
|
|
16
18
|
:note="info?.note"
|
|
17
19
|
:note-html="info?.noteHTML"
|
|
20
|
+
:clicks-context="clicksContext"
|
|
18
21
|
/>
|
|
19
22
|
</template>
|
|
@@ -4,25 +4,28 @@ import { computed } from 'vue'
|
|
|
4
4
|
import { provideLocal } from '@vueuse/core'
|
|
5
5
|
import { configs, slideAspect, slideWidth } from '../env'
|
|
6
6
|
import { injectionSlideScale } from '../constants'
|
|
7
|
-
import {
|
|
7
|
+
import { useNav } from '../composables/useNav'
|
|
8
8
|
import PrintSlide from './PrintSlide.vue'
|
|
9
9
|
|
|
10
10
|
const props = defineProps<{
|
|
11
11
|
width: number
|
|
12
12
|
}>()
|
|
13
13
|
|
|
14
|
+
const { slides, currentRoute } = useNav()
|
|
15
|
+
|
|
14
16
|
const width = computed(() => props.width)
|
|
15
|
-
const height = computed(() => props.width / slideAspect)
|
|
17
|
+
const height = computed(() => props.width / slideAspect.value)
|
|
16
18
|
|
|
17
19
|
const screenAspect = computed(() => width.value / height.value)
|
|
18
20
|
|
|
19
21
|
const scale = computed(() => {
|
|
20
|
-
if (screenAspect.value < slideAspect)
|
|
21
|
-
return width.value / slideWidth
|
|
22
|
-
return (height.value * slideAspect) / slideWidth
|
|
22
|
+
if (screenAspect.value < slideAspect.value)
|
|
23
|
+
return width.value / slideWidth.value
|
|
24
|
+
return (height.value * slideAspect.value) / slideWidth.value
|
|
23
25
|
})
|
|
24
26
|
|
|
25
|
-
|
|
27
|
+
// In print mode, the routes will never change. So we don't need reactivity here.
|
|
28
|
+
let routes = slides.value
|
|
26
29
|
if (currentRoute.value.query.range) {
|
|
27
30
|
const r = parseRangeString(routes.length, currentRoute.value.query.range as string)
|
|
28
31
|
routes = r.map(i => routes[i - 1])
|
|
@@ -38,7 +41,7 @@ provideLocal(injectionSlideScale, scale)
|
|
|
38
41
|
<template>
|
|
39
42
|
<div id="print-container" :class="className">
|
|
40
43
|
<div id="print-content">
|
|
41
|
-
<PrintSlide v-for="route of routes" :key="route.
|
|
44
|
+
<PrintSlide v-for="route of routes" :key="route.no" :route="route" />
|
|
42
45
|
</div>
|
|
43
46
|
<slot name="controls" />
|
|
44
47
|
</div>
|
|
@@ -50,6 +53,6 @@ provideLocal(injectionSlideScale, scale)
|
|
|
50
53
|
}
|
|
51
54
|
|
|
52
55
|
.print-slide-container {
|
|
53
|
-
@apply relative overflow-hidden break-after-page;
|
|
56
|
+
@apply relative overflow-hidden break-after-page translate-0;
|
|
54
57
|
}
|
|
55
58
|
</style>
|
package/internals/PrintSlide.vue
CHANGED
|
@@ -1,24 +1,23 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import type {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { useFixedClicks } from '../composables/useClicks'
|
|
2
|
+
import type { SlideRoute } from '@slidev/types'
|
|
3
|
+
import { useFixedNav } from '../composables/useNav'
|
|
4
|
+
import { createFixedClicks } from '../composables/useClicks'
|
|
6
5
|
import PrintSlideClick from './PrintSlideClick.vue'
|
|
7
6
|
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
const route = computed(() => props.route)
|
|
11
|
-
const nav = useNav(route)
|
|
12
|
-
const clicks0 = useFixedClicks(route.value, 0)[1]
|
|
7
|
+
const { route } = defineProps<{ route: SlideRoute }>()
|
|
8
|
+
const clicks0 = createFixedClicks(route, 0)
|
|
13
9
|
</script>
|
|
14
10
|
|
|
15
11
|
<template>
|
|
16
12
|
<PrintSlideClick
|
|
17
13
|
:clicks-context="clicks0"
|
|
18
|
-
:nav="
|
|
19
|
-
:route="route"
|
|
14
|
+
:nav="useFixedNav(route, clicks0)"
|
|
20
15
|
/>
|
|
21
16
|
<template v-if="!clicks0.disabled">
|
|
22
|
-
<PrintSlideClick
|
|
17
|
+
<PrintSlideClick
|
|
18
|
+
v-for="i of clicks0.total"
|
|
19
|
+
:key="i"
|
|
20
|
+
:nav="useFixedNav(route, createFixedClicks(route, i))"
|
|
21
|
+
/>
|
|
23
22
|
</template>
|
|
24
23
|
</template>
|