@slidev/client 0.48.0-beta.2 → 0.48.0-beta.21
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 +14 -6
- package/builtin/KaTexBlockWrapper.vue +5 -4
- package/builtin/Mermaid.vue +4 -3
- package/builtin/Monaco.vue +109 -92
- package/builtin/RenderWhen.vue +3 -3
- package/builtin/ShikiMagicMove.vue +50 -0
- package/builtin/SlideCurrentNo.vue +2 -3
- package/builtin/SlidesTotal.vue +3 -4
- package/builtin/SlidevVideo.vue +9 -7
- package/builtin/Toc.vue +4 -4
- package/builtin/TocList.vue +4 -3
- package/builtin/Tweet.vue +3 -22
- package/builtin/VClick.ts +2 -1
- package/builtin/VClickGap.vue +3 -5
- package/builtin/VClicks.ts +1 -1
- package/composables/useClicks.ts +39 -20
- package/composables/useContext.ts +4 -9
- package/composables/useNav.ts +182 -44
- package/composables/useSwipeControls.ts +40 -0
- package/composables/useTocTree.ts +63 -0
- package/constants.ts +59 -10
- package/context.ts +73 -0
- package/env.ts +3 -12
- package/internals/ClicksSlider.vue +93 -0
- package/internals/Controls.vue +2 -2
- package/internals/DrawingControls.vue +39 -9
- package/internals/DrawingLayer.vue +3 -3
- package/internals/Goto.vue +7 -6
- package/internals/IconButton.vue +7 -3
- package/internals/InfoDialog.vue +1 -1
- package/internals/Modal.vue +1 -1
- package/internals/NavControls.vue +11 -10
- package/internals/NoteDisplay.vue +131 -8
- package/internals/NoteEditable.vue +128 -0
- package/internals/NoteStatic.vue +8 -6
- package/internals/PrintContainer.vue +8 -6
- package/internals/PrintSlide.vue +10 -11
- package/internals/PrintSlideClick.vue +14 -18
- package/internals/{SlidesOverview.vue → QuickOverview.vue} +31 -20
- package/internals/RecordingControls.vue +1 -1
- package/internals/RecordingDialog.vue +5 -6
- package/internals/{Editor.vue → SideEditor.vue} +9 -5
- package/internals/SlideContainer.vue +12 -9
- package/internals/SlideLoading.vue +19 -0
- package/internals/SlideWrapper.ts +32 -16
- package/internals/SlidesShow.vue +20 -18
- package/layouts/error.vue +5 -0
- package/layouts/two-cols-header.vue +9 -3
- package/logic/drawings.ts +13 -10
- package/logic/nav-state.ts +20 -0
- package/logic/nav.ts +51 -258
- package/logic/note.ts +9 -9
- package/logic/overview.ts +2 -2
- package/logic/route.ts +10 -1
- package/logic/slides.ts +19 -0
- package/logic/transition.ts +50 -0
- package/main.ts +8 -4
- package/modules/context.ts +7 -13
- package/modules/mermaid.ts +6 -7
- package/modules/{directives.ts → v-click.ts} +15 -15
- package/modules/v-mark.ts +159 -0
- package/package.json +27 -16
- package/{internals/EntrySelect.vue → pages/entry.vue} +7 -0
- package/{internals/NotesView.vue → pages/notes.vue} +7 -6
- package/pages/overview.vue +227 -0
- package/{internals/Play.vue → pages/play.vue} +17 -13
- package/{internals/PresenterPrint.vue → pages/presenter/print.vue} +13 -8
- package/{internals/Presenter.vue → pages/presenter.vue} +114 -105
- package/{internals/Print.vue → pages/print.vue} +3 -4
- package/routes.ts +28 -60
- package/setup/codemirror.ts +8 -3
- package/setup/monaco.ts +108 -44
- package/setup/root.ts +8 -9
- package/setup/shortcuts.ts +2 -1
- package/shim-vue.d.ts +38 -0
- package/shim.d.ts +1 -13
- package/state/index.ts +10 -10
- package/styles/code.css +7 -3
- package/styles/index.css +68 -7
- package/styles/katex.css +1 -1
- package/styles/layouts-base.css +17 -12
- package/styles/monaco.css +27 -0
- package/styles/vars.css +1 -0
- package/uno.config.ts +14 -2
- package/utils.ts +15 -2
- 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 -88
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { ClicksContext } from '@slidev/types'
|
|
3
|
+
import { computed } from 'vue'
|
|
4
|
+
|
|
5
|
+
const props = defineProps<{
|
|
6
|
+
clicksContext: ClicksContext
|
|
7
|
+
}>()
|
|
8
|
+
|
|
9
|
+
const total = computed(() => props.clicksContext.total)
|
|
10
|
+
const current = computed({
|
|
11
|
+
get() {
|
|
12
|
+
return props.clicksContext.current > total.value ? -1 : props.clicksContext.current
|
|
13
|
+
},
|
|
14
|
+
set(value: number) {
|
|
15
|
+
// eslint-disable-next-line vue/no-mutating-props
|
|
16
|
+
props.clicksContext.current = value
|
|
17
|
+
},
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const range = computed(() => Array.from({ length: total.value + 1 }, (_, i) => i))
|
|
21
|
+
|
|
22
|
+
function onMousedown() {
|
|
23
|
+
if (current.value < 0 || current.value > total.value)
|
|
24
|
+
current.value = 0
|
|
25
|
+
}
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<template>
|
|
29
|
+
<div
|
|
30
|
+
class="flex gap-0.5 items-center select-none"
|
|
31
|
+
:title="`Clicks in this slide: ${total}`"
|
|
32
|
+
:class="total ? '' : 'op50'"
|
|
33
|
+
>
|
|
34
|
+
<div class="flex gap-1 items-center min-w-16">
|
|
35
|
+
<carbon:cursor-1 text-sm op50 />
|
|
36
|
+
<span text-primary>{{ current }}</span>
|
|
37
|
+
<span op50>/</span>
|
|
38
|
+
<span op50>{{ total }}</span>
|
|
39
|
+
</div>
|
|
40
|
+
<div
|
|
41
|
+
relative flex-auto h5 flex="~"
|
|
42
|
+
@dblclick="current = clicksContext.total"
|
|
43
|
+
>
|
|
44
|
+
<div
|
|
45
|
+
v-for="i of range" :key="i"
|
|
46
|
+
border="y main" of-hidden relative
|
|
47
|
+
:class="[
|
|
48
|
+
i === 0 ? 'rounded-l border-l' : '',
|
|
49
|
+
i === total ? 'rounded-r border-r' : '',
|
|
50
|
+
]"
|
|
51
|
+
:style="{ width: total > 0 ? `${1 / total * 100}%` : '100%' }"
|
|
52
|
+
>
|
|
53
|
+
<div absolute inset-0 :class="i <= current ? 'bg-primary op20' : ''" />
|
|
54
|
+
<div
|
|
55
|
+
:class="[
|
|
56
|
+
+i === +current ? 'text-primary font-bold op100 border-primary' : 'op30 border-main',
|
|
57
|
+
i === 0 ? 'rounded-l' : '',
|
|
58
|
+
i === total ? 'rounded-r' : 'border-r-2',
|
|
59
|
+
]"
|
|
60
|
+
w-full h-full text-xs flex items-center justify-center z-1
|
|
61
|
+
>
|
|
62
|
+
{{ i }}
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
<input
|
|
66
|
+
v-model="current"
|
|
67
|
+
class="range" absolute inset-0
|
|
68
|
+
type="range" :min="0" :max="total" :step="1" z-10 op0
|
|
69
|
+
:style="{ '--thumb-width': `${1 / (total + 1) * 100}%` }"
|
|
70
|
+
@mousedown="onMousedown"
|
|
71
|
+
@focus="event => (event.currentTarget as HTMLElement)?.blur()"
|
|
72
|
+
>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
</template>
|
|
76
|
+
|
|
77
|
+
<style scoped>
|
|
78
|
+
.range {
|
|
79
|
+
-webkit-appearance: none;
|
|
80
|
+
appearance: none;
|
|
81
|
+
background: transparent;
|
|
82
|
+
}
|
|
83
|
+
.range::-webkit-slider-thumb {
|
|
84
|
+
-webkit-appearance: none;
|
|
85
|
+
height: 100%;
|
|
86
|
+
width: var(--thumb-width, 0.5rem);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.range::-moz-range-thumb {
|
|
90
|
+
height: 100%;
|
|
91
|
+
width: var(--thumb-width, 0.5rem);
|
|
92
|
+
}
|
|
93
|
+
</style>
|
package/internals/Controls.vue
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { shallowRef } from 'vue'
|
|
3
3
|
import { showInfoDialog, showOverview, showRecordingDialog } from '../state'
|
|
4
4
|
import { configs } from '../env'
|
|
5
|
-
import
|
|
5
|
+
import QuickOverview from './QuickOverview.vue'
|
|
6
6
|
import InfoDialog from './InfoDialog.vue'
|
|
7
7
|
import Goto from './Goto.vue'
|
|
8
8
|
|
|
@@ -15,7 +15,7 @@ if (__SLIDEV_FEATURE_RECORD__) {
|
|
|
15
15
|
</script>
|
|
16
16
|
|
|
17
17
|
<template>
|
|
18
|
-
<
|
|
18
|
+
<QuickOverview v-model="showOverview" />
|
|
19
19
|
<Goto />
|
|
20
20
|
<WebCamera v-if="WebCamera" />
|
|
21
21
|
<RecordingDialog v-if="RecordingDialog" v-model="showRecordingDialog" />
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { Menu } from 'floating-vue'
|
|
3
|
+
import 'floating-vue/dist/style.css'
|
|
2
4
|
import {
|
|
3
5
|
brush,
|
|
4
6
|
brushColors,
|
|
@@ -21,20 +23,24 @@ function undo() {
|
|
|
21
23
|
function redo() {
|
|
22
24
|
drauu.redo()
|
|
23
25
|
}
|
|
26
|
+
|
|
27
|
+
let lastDrawingMode: typeof drawingMode.value = 'stylus'
|
|
24
28
|
function setDrawingMode(mode: typeof drawingMode.value) {
|
|
25
29
|
drawingMode.value = mode
|
|
26
30
|
drawingEnabled.value = true
|
|
31
|
+
if (mode !== 'eraseLine')
|
|
32
|
+
lastDrawingMode = mode
|
|
27
33
|
}
|
|
28
34
|
function setBrushColor(color: typeof brush.color) {
|
|
29
35
|
brush.color = color
|
|
30
36
|
drawingEnabled.value = true
|
|
37
|
+
drawingMode.value = lastDrawingMode
|
|
31
38
|
}
|
|
32
39
|
</script>
|
|
33
40
|
|
|
34
41
|
<template>
|
|
35
42
|
<Draggable
|
|
36
|
-
class="flex flex-wrap text-xl p-2 gap-1 rounded-md bg-main shadow transition-opacity duration-200"
|
|
37
|
-
dark="border border-gray-400 border-opacity-10"
|
|
43
|
+
class="flex flex-wrap text-xl p-2 gap-1 rounded-md bg-main shadow transition-opacity duration-200 z-20 border border-main"
|
|
38
44
|
:class="drawingEnabled ? '' : drawingPinned ? 'opacity-40 hover:opacity-90' : 'opacity-0 pointer-events-none'"
|
|
39
45
|
storage-key="slidev-drawing-pos"
|
|
40
46
|
:initial-x="10"
|
|
@@ -57,23 +63,41 @@ function setBrushColor(color: typeof brush.color) {
|
|
|
57
63
|
<IconButton title="Draw a rectangle" :class="{ shallow: drawingMode !== 'rectangle' }" @click="setDrawingMode('rectangle')">
|
|
58
64
|
<carbon:checkbox />
|
|
59
65
|
</IconButton>
|
|
60
|
-
|
|
61
|
-
<!-- <IconButton title="Erase" :class="{ shallow: drawingMode != 'eraseLine' }" @click="setDrawingMode('eraseLine')">
|
|
66
|
+
<IconButton title="Erase" :class="{ shallow: drawingMode !== 'eraseLine' }" @click="setDrawingMode('eraseLine')">
|
|
62
67
|
<carbon:erase />
|
|
63
|
-
</IconButton>
|
|
68
|
+
</IconButton>
|
|
64
69
|
|
|
65
70
|
<VerticalDivider />
|
|
66
71
|
|
|
72
|
+
<Menu>
|
|
73
|
+
<IconButton title="Adjust stroke width" :class="{ shallow: drawingMode === 'eraseLine' }">
|
|
74
|
+
<svg viewBox="0 0 32 32" width="1.2em" height="1.2em">
|
|
75
|
+
<line x1="2" y1="15" x2="22" y2="4" stroke="currentColor" stroke-width="1" stroke-linecap="round" />
|
|
76
|
+
<line x1="2" y1="24" x2="28" y2="10" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
|
|
77
|
+
<line x1="7" y1="31" x2="29" y2="19" stroke="currentColor" stroke-width="3" stroke-linecap="round" />
|
|
78
|
+
</svg>
|
|
79
|
+
</IconButton>
|
|
80
|
+
<template #popper>
|
|
81
|
+
<div class="flex bg-main p-2">
|
|
82
|
+
<div class="inline-block w-7 text-center">
|
|
83
|
+
{{ brush.size }}
|
|
84
|
+
</div>
|
|
85
|
+
<div class="pt-.5">
|
|
86
|
+
<input v-model="brush.size" type="range" min="1" max="15" @change="drawingMode = lastDrawingMode">
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
</template>
|
|
90
|
+
</Menu>
|
|
67
91
|
<IconButton
|
|
68
92
|
v-for="color of brushColors"
|
|
69
93
|
:key="color"
|
|
70
94
|
title="Set brush color"
|
|
71
|
-
:class="brush.color === color ? 'active' : 'shallow'"
|
|
95
|
+
:class="brush.color === color && drawingMode !== 'eraseLine' ? 'active' : 'shallow'"
|
|
72
96
|
@click="setBrushColor(color)"
|
|
73
97
|
>
|
|
74
98
|
<div
|
|
75
|
-
class="w-6 h-6 transition-all transform border
|
|
76
|
-
:class="brush.color !== color ? 'rounded-1/2 scale-85' : 'rounded-md'"
|
|
99
|
+
class="w-6 h-6 transition-all transform border"
|
|
100
|
+
:class="brush.color !== color ? 'rounded-1/2 scale-85 border-white' : 'rounded-md border-gray-300/50'"
|
|
77
101
|
:style="drawingEnabled ? { background: color } : { borderColor: color }"
|
|
78
102
|
/>
|
|
79
103
|
</IconButton>
|
|
@@ -87,7 +111,7 @@ function setBrushColor(color: typeof brush.color) {
|
|
|
87
111
|
<carbon:redo />
|
|
88
112
|
</IconButton>
|
|
89
113
|
<IconButton title="Delete" :class="{ disabled: !canClear }" @click="clearDrauu()">
|
|
90
|
-
<carbon:
|
|
114
|
+
<carbon:trash-can />
|
|
91
115
|
</IconButton>
|
|
92
116
|
|
|
93
117
|
<VerticalDivider />
|
|
@@ -106,3 +130,9 @@ function setBrushColor(color: typeof brush.color) {
|
|
|
106
130
|
</IconButton>
|
|
107
131
|
</Draggable>
|
|
108
132
|
</template>
|
|
133
|
+
|
|
134
|
+
<style>
|
|
135
|
+
.v-popper--theme-menu .v-popper__arrow-inner {
|
|
136
|
+
--uno: border-main;
|
|
137
|
+
}
|
|
138
|
+
</style>
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import {
|
|
2
|
+
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|
3
3
|
import { drauu, drawingEnabled, loadCanvas } from '../logic/drawings'
|
|
4
|
-
import {
|
|
4
|
+
import { useSlideContext } from '../context'
|
|
5
5
|
|
|
6
|
-
const scale =
|
|
6
|
+
const scale = useSlideContext().$scale
|
|
7
7
|
const svg = ref<SVGSVGElement>()
|
|
8
8
|
|
|
9
9
|
onMounted(() => {
|
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,
|
|
4
|
+
import { go, slides } from '../logic/nav'
|
|
5
5
|
import { activeElement, showGotoDialog } from '../state'
|
|
6
|
-
import Titles from '
|
|
6
|
+
import Titles from '#slidev/titles.md'
|
|
7
7
|
|
|
8
8
|
const container = ref<HTMLDivElement>()
|
|
9
9
|
const input = ref<HTMLInputElement>()
|
|
@@ -16,7 +16,7 @@ function notNull<T>(value: T | null | undefined): value is T {
|
|
|
16
16
|
return value !== null && value !== undefined
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
const fuse = computed(() => new Fuse(
|
|
19
|
+
const fuse = computed(() => new Fuse(slides.value.map(i => i.meta?.slide).filter(notNull), {
|
|
20
20
|
keys: ['no', 'title'],
|
|
21
21
|
threshold: 0.3,
|
|
22
22
|
shouldSort: true,
|
|
@@ -165,10 +165,11 @@ watch(activeElement, () => {
|
|
|
165
165
|
</div>
|
|
166
166
|
</template>
|
|
167
167
|
|
|
168
|
-
<style scoped
|
|
168
|
+
<style scoped>
|
|
169
169
|
.autocomplete-list {
|
|
170
|
-
|
|
171
|
-
|
|
170
|
+
--uno: bg-main mt-1;
|
|
171
|
+
overflow: auto;
|
|
172
|
+
max-height: calc(100vh - 100px);
|
|
172
173
|
}
|
|
173
174
|
|
|
174
175
|
.autocomplete {
|
package/internals/IconButton.vue
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
defineProps<{
|
|
3
3
|
title: string
|
|
4
|
+
icon?: string
|
|
5
|
+
as?: string
|
|
4
6
|
}>()
|
|
5
7
|
</script>
|
|
6
8
|
|
|
7
9
|
<template>
|
|
8
|
-
<button class="slidev-icon-btn" :title="title" v-bind="$attrs">
|
|
10
|
+
<component :is="as || 'button'" class="slidev-icon-btn" :title="title" v-bind="$attrs">
|
|
9
11
|
<span class="sr-only">{{ title }}</span>
|
|
10
|
-
<slot
|
|
11
|
-
|
|
12
|
+
<slot>
|
|
13
|
+
<div :class="icon" />
|
|
14
|
+
</slot>
|
|
15
|
+
</component>
|
|
12
16
|
</template>
|
package/internals/InfoDialog.vue
CHANGED
|
@@ -10,7 +10,7 @@ const props = defineProps({
|
|
|
10
10
|
},
|
|
11
11
|
})
|
|
12
12
|
|
|
13
|
-
const emit = defineEmits
|
|
13
|
+
const emit = defineEmits(['update:modelValue'])
|
|
14
14
|
const value = useVModel(props, 'modelValue', emit)
|
|
15
15
|
const hasInfo = computed(() => typeof configs.info === 'string')
|
|
16
16
|
</script>
|
package/internals/Modal.vue
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
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
|
+
import { currentRoute, currentSlideNo, getSlidePath, hasNext, hasPrev, isEmbedded, isPresenter, isPresenterAvailable, next, prev, total } from '../logic/nav'
|
|
5
6
|
import { activeElement, breakpoints, fullscreen, presenterLayout, showEditor, showInfoDialog, showPresenterCursor, toggleOverview, togglePresenterLayout } from '../state'
|
|
6
7
|
import { brush, drawingEnabled } from '../logic/drawings'
|
|
7
8
|
import { configs } from '../env'
|
|
@@ -10,8 +11,7 @@ import MenuButton from './MenuButton.vue'
|
|
|
10
11
|
import VerticalDivider from './VerticalDivider.vue'
|
|
11
12
|
import IconButton from './IconButton.vue'
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
import CustomNavControls from '/@slidev/custom-nav-controls'
|
|
14
|
+
import CustomNavControls from '#slidev/custom-nav-controls'
|
|
15
15
|
|
|
16
16
|
const props = defineProps({
|
|
17
17
|
persist: {
|
|
@@ -22,9 +22,10 @@ const props = defineProps({
|
|
|
22
22
|
const md = breakpoints.smaller('md')
|
|
23
23
|
const { isFullscreen, toggle: toggleFullscreen } = fullscreen
|
|
24
24
|
|
|
25
|
+
const presenterPassword = computed(() => currentRoute.value.query.password)
|
|
25
26
|
const query = computed(() => presenterPassword.value ? `?password=${presenterPassword.value}` : '')
|
|
26
|
-
const presenterLink = computed(() =>
|
|
27
|
-
const nonPresenterLink = computed(() =>
|
|
27
|
+
const presenterLink = computed(() => `${getSlidePath(currentSlideNo.value, true)}${query.value}`)
|
|
28
|
+
const nonPresenterLink = computed(() => `${getSlidePath(currentSlideNo.value, false)}${query.value}`)
|
|
28
29
|
|
|
29
30
|
const root = ref<HTMLDivElement>()
|
|
30
31
|
function onMouseLeave() {
|
|
@@ -34,7 +35,7 @@ function onMouseLeave() {
|
|
|
34
35
|
|
|
35
36
|
const barStyle = computed(() => props.persist
|
|
36
37
|
? 'text-$slidev-controls-foreground bg-transparent'
|
|
37
|
-
: 'rounded-md bg-main shadow dark:border dark:border-
|
|
38
|
+
: 'rounded-md bg-main shadow dark:border dark:border-main')
|
|
38
39
|
|
|
39
40
|
const RecordingControls = shallowRef<any>()
|
|
40
41
|
if (__SLIDEV_FEATURE_RECORD__)
|
|
@@ -108,20 +109,20 @@ if (__SLIDEV_FEATURE_DRAWINGS__)
|
|
|
108
109
|
<RouterLink v-if="isPresenter" :to="nonPresenterLink" class="slidev-icon-btn" title="Play Mode">
|
|
109
110
|
<carbon:presentation-file />
|
|
110
111
|
</RouterLink>
|
|
111
|
-
<RouterLink v-if="__SLIDEV_FEATURE_PRESENTER__ &&
|
|
112
|
+
<RouterLink v-if="__SLIDEV_FEATURE_PRESENTER__ && isPresenterAvailable" :to="presenterLink" class="slidev-icon-btn" title="Presenter Mode">
|
|
112
113
|
<carbon:user-speaker />
|
|
113
114
|
</RouterLink>
|
|
114
115
|
|
|
115
116
|
<IconButton
|
|
116
117
|
v-if="__DEV__ && __SLIDEV_FEATURE_EDITOR__"
|
|
117
118
|
:title="showEditor ? 'Hide editor' : 'Show editor'"
|
|
118
|
-
class="
|
|
119
|
+
class="lt-md:hidden"
|
|
119
120
|
@click="showEditor = !showEditor"
|
|
120
121
|
>
|
|
121
122
|
<carbon:text-annotation-toggle />
|
|
122
123
|
</IconButton>
|
|
123
124
|
|
|
124
|
-
<IconButton v-if="isPresenter" title="Toggle Presenter Layout" @click="togglePresenterLayout">
|
|
125
|
+
<IconButton v-if="isPresenter" title="Toggle Presenter Layout" class="aspect-ratio-initial" @click="togglePresenterLayout">
|
|
125
126
|
<carbon:template />
|
|
126
127
|
{{ presenterLayout }}
|
|
127
128
|
</IconButton>
|
|
@@ -157,7 +158,7 @@ if (__SLIDEV_FEATURE_DRAWINGS__)
|
|
|
157
158
|
|
|
158
159
|
<div class="h-40px flex" p="l-1 t-0.5 r-2" text="sm leading-2">
|
|
159
160
|
<div class="my-auto">
|
|
160
|
-
{{
|
|
161
|
+
{{ currentSlideNo }}
|
|
161
162
|
<span class="opacity-50">/ {{ total }}</span>
|
|
162
163
|
</div>
|
|
163
164
|
</div>
|
|
@@ -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"
|
|
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,128 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { PropType } from 'vue'
|
|
3
|
+
import { nextTick, ref, watch, watchEffect } from 'vue'
|
|
4
|
+
import { ignorableWatch, onClickOutside, useVModel } from '@vueuse/core'
|
|
5
|
+
import type { ClicksContext } from '@slidev/types'
|
|
6
|
+
import { useDynamicSlideInfo } from '../logic/note'
|
|
7
|
+
import NoteDisplay from './NoteDisplay.vue'
|
|
8
|
+
|
|
9
|
+
const props = defineProps({
|
|
10
|
+
no: {
|
|
11
|
+
type: Number,
|
|
12
|
+
},
|
|
13
|
+
class: {
|
|
14
|
+
default: '',
|
|
15
|
+
},
|
|
16
|
+
editing: {
|
|
17
|
+
default: false,
|
|
18
|
+
},
|
|
19
|
+
style: {
|
|
20
|
+
default: () => ({}),
|
|
21
|
+
},
|
|
22
|
+
placeholder: {
|
|
23
|
+
default: 'No notes for this slide',
|
|
24
|
+
},
|
|
25
|
+
clicksContext: {
|
|
26
|
+
type: Object as PropType<ClicksContext>,
|
|
27
|
+
},
|
|
28
|
+
autoHeight: {
|
|
29
|
+
default: false,
|
|
30
|
+
},
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const emit = defineEmits<{
|
|
34
|
+
(type: 'update:editing', value: boolean): void
|
|
35
|
+
(type: 'markerDblclick', e: MouseEvent, clicks: number): void
|
|
36
|
+
(type: 'markerClick', e: MouseEvent, clicks: number): void
|
|
37
|
+
}>()
|
|
38
|
+
|
|
39
|
+
const editing = useVModel(props, 'editing', emit, { passive: true })
|
|
40
|
+
|
|
41
|
+
const { info, update } = useDynamicSlideInfo(props.no)
|
|
42
|
+
|
|
43
|
+
const note = ref('')
|
|
44
|
+
let timer: any
|
|
45
|
+
|
|
46
|
+
// Send back the note on changes
|
|
47
|
+
const { ignoreUpdates } = ignorableWatch(
|
|
48
|
+
note,
|
|
49
|
+
(v) => {
|
|
50
|
+
if (!editing.value)
|
|
51
|
+
return
|
|
52
|
+
const id = props.no
|
|
53
|
+
clearTimeout(timer)
|
|
54
|
+
timer = setTimeout(() => {
|
|
55
|
+
update({ note: v }, id)
|
|
56
|
+
}, 500)
|
|
57
|
+
},
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
// Update note value when info changes
|
|
61
|
+
watch(
|
|
62
|
+
() => info.value?.note,
|
|
63
|
+
(value = '') => {
|
|
64
|
+
if (editing.value)
|
|
65
|
+
return
|
|
66
|
+
clearTimeout(timer)
|
|
67
|
+
ignoreUpdates(() => {
|
|
68
|
+
note.value = value
|
|
69
|
+
})
|
|
70
|
+
},
|
|
71
|
+
{ immediate: true, flush: 'sync' },
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
const inputEl = ref<HTMLTextAreaElement>()
|
|
75
|
+
const inputHeight = ref<number | null>()
|
|
76
|
+
|
|
77
|
+
watchEffect(() => {
|
|
78
|
+
if (editing.value)
|
|
79
|
+
inputEl.value?.focus()
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
onClickOutside(inputEl, () => {
|
|
83
|
+
editing.value = false
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
function calculateEditorHeight() {
|
|
87
|
+
if (!props.autoHeight || !inputEl.value || !editing.value)
|
|
88
|
+
return
|
|
89
|
+
if (inputEl.value.scrollHeight > inputEl.value.clientHeight)
|
|
90
|
+
inputEl.value.style.height = `${inputEl.value.scrollHeight}px`
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
watch(
|
|
94
|
+
[note, editing],
|
|
95
|
+
() => {
|
|
96
|
+
nextTick(() => {
|
|
97
|
+
calculateEditorHeight()
|
|
98
|
+
})
|
|
99
|
+
},
|
|
100
|
+
{ flush: 'post', immediate: true },
|
|
101
|
+
)
|
|
102
|
+
</script>
|
|
103
|
+
|
|
104
|
+
<template>
|
|
105
|
+
<NoteDisplay
|
|
106
|
+
v-if="!editing"
|
|
107
|
+
class="border-transparent border-2"
|
|
108
|
+
:class="[props.class, note ? '' : 'opacity-25 italic select-none']"
|
|
109
|
+
:style="props.style"
|
|
110
|
+
:note="note || placeholder"
|
|
111
|
+
:note-html="info?.noteHTML"
|
|
112
|
+
:clicks-context="clicksContext"
|
|
113
|
+
:auto-scroll="!autoHeight"
|
|
114
|
+
@marker-click="(e, clicks) => emit('markerClick', e, clicks)"
|
|
115
|
+
@marker-dblclick="(e, clicks) => emit('markerDblclick', e, clicks)"
|
|
116
|
+
/>
|
|
117
|
+
<textarea
|
|
118
|
+
v-else
|
|
119
|
+
ref="inputEl"
|
|
120
|
+
v-model="note"
|
|
121
|
+
class="prose resize-none overflow-auto outline-none bg-transparent block border-primary border-2"
|
|
122
|
+
style="line-height: 1.75;"
|
|
123
|
+
:style="[props.style, inputHeight != null ? { height: `${inputHeight}px` } : {}]"
|
|
124
|
+
:class="props.class"
|
|
125
|
+
:placeholder="placeholder"
|
|
126
|
+
@keydown.esc="editing = false"
|
|
127
|
+
/>
|
|
128
|
+
</template>
|