@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.
Files changed (91) hide show
  1. package/App.vue +7 -0
  2. package/builtin/Arrow.vue +2 -4
  3. package/builtin/CodeBlockWrapper.vue +14 -6
  4. package/builtin/KaTexBlockWrapper.vue +5 -4
  5. package/builtin/Mermaid.vue +4 -3
  6. package/builtin/Monaco.vue +109 -92
  7. package/builtin/RenderWhen.vue +3 -3
  8. package/builtin/ShikiMagicMove.vue +50 -0
  9. package/builtin/SlideCurrentNo.vue +2 -3
  10. package/builtin/SlidesTotal.vue +3 -4
  11. package/builtin/SlidevVideo.vue +9 -7
  12. package/builtin/Toc.vue +4 -4
  13. package/builtin/TocList.vue +4 -3
  14. package/builtin/Tweet.vue +3 -22
  15. package/builtin/VClick.ts +2 -1
  16. package/builtin/VClickGap.vue +3 -5
  17. package/builtin/VClicks.ts +1 -1
  18. package/composables/useClicks.ts +39 -20
  19. package/composables/useContext.ts +4 -9
  20. package/composables/useNav.ts +182 -44
  21. package/composables/useSwipeControls.ts +40 -0
  22. package/composables/useTocTree.ts +63 -0
  23. package/constants.ts +59 -10
  24. package/context.ts +73 -0
  25. package/env.ts +3 -12
  26. package/internals/ClicksSlider.vue +93 -0
  27. package/internals/Controls.vue +2 -2
  28. package/internals/DrawingControls.vue +39 -9
  29. package/internals/DrawingLayer.vue +3 -3
  30. package/internals/Goto.vue +7 -6
  31. package/internals/IconButton.vue +7 -3
  32. package/internals/InfoDialog.vue +1 -1
  33. package/internals/Modal.vue +1 -1
  34. package/internals/NavControls.vue +11 -10
  35. package/internals/NoteDisplay.vue +131 -8
  36. package/internals/NoteEditable.vue +128 -0
  37. package/internals/NoteStatic.vue +8 -6
  38. package/internals/PrintContainer.vue +8 -6
  39. package/internals/PrintSlide.vue +10 -11
  40. package/internals/PrintSlideClick.vue +14 -18
  41. package/internals/{SlidesOverview.vue → QuickOverview.vue} +31 -20
  42. package/internals/RecordingControls.vue +1 -1
  43. package/internals/RecordingDialog.vue +5 -6
  44. package/internals/{Editor.vue → SideEditor.vue} +9 -5
  45. package/internals/SlideContainer.vue +12 -9
  46. package/internals/SlideLoading.vue +19 -0
  47. package/internals/SlideWrapper.ts +32 -16
  48. package/internals/SlidesShow.vue +20 -18
  49. package/layouts/error.vue +5 -0
  50. package/layouts/two-cols-header.vue +9 -3
  51. package/logic/drawings.ts +13 -10
  52. package/logic/nav-state.ts +20 -0
  53. package/logic/nav.ts +51 -258
  54. package/logic/note.ts +9 -9
  55. package/logic/overview.ts +2 -2
  56. package/logic/route.ts +10 -1
  57. package/logic/slides.ts +19 -0
  58. package/logic/transition.ts +50 -0
  59. package/main.ts +8 -4
  60. package/modules/context.ts +7 -13
  61. package/modules/mermaid.ts +6 -7
  62. package/modules/{directives.ts → v-click.ts} +15 -15
  63. package/modules/v-mark.ts +159 -0
  64. package/package.json +27 -16
  65. package/{internals/EntrySelect.vue → pages/entry.vue} +7 -0
  66. package/{internals/NotesView.vue → pages/notes.vue} +7 -6
  67. package/pages/overview.vue +227 -0
  68. package/{internals/Play.vue → pages/play.vue} +17 -13
  69. package/{internals/PresenterPrint.vue → pages/presenter/print.vue} +13 -8
  70. package/{internals/Presenter.vue → pages/presenter.vue} +114 -105
  71. package/{internals/Print.vue → pages/print.vue} +3 -4
  72. package/routes.ts +28 -60
  73. package/setup/codemirror.ts +8 -3
  74. package/setup/monaco.ts +108 -44
  75. package/setup/root.ts +8 -9
  76. package/setup/shortcuts.ts +2 -1
  77. package/shim-vue.d.ts +38 -0
  78. package/shim.d.ts +1 -13
  79. package/state/index.ts +10 -10
  80. package/styles/code.css +7 -3
  81. package/styles/index.css +68 -7
  82. package/styles/katex.css +1 -1
  83. package/styles/layouts-base.css +17 -12
  84. package/styles/monaco.css +27 -0
  85. package/styles/vars.css +1 -0
  86. package/uno.config.ts +14 -2
  87. package/utils.ts +15 -2
  88. package/iframes/monaco/index.css +0 -28
  89. package/iframes/monaco/index.html +0 -7
  90. package/iframes/monaco/index.ts +0 -260
  91. 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>
@@ -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 SlidesOverview from './SlidesOverview.vue'
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
- <SlidesOverview v-model="showOverview" />
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
- <!-- TODO: not sure why it's not working! -->
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 border-gray-400/50"
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:delete />
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 { inject, onBeforeUnmount, onMounted, ref, watch } from 'vue'
2
+ import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
3
3
  import { drauu, drawingEnabled, loadCanvas } from '../logic/drawings'
4
- import { injectionSlideScale } from '../constants'
4
+ import { useSlideContext } from '../context'
5
5
 
6
- const scale = inject(injectionSlideScale)!
6
+ const scale = useSlideContext().$scale
7
7
  const svg = ref<SVGSVGElement>()
8
8
 
9
9
  onMounted(() => {
@@ -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'
4
+ import { go, slides } from '../logic/nav'
5
5
  import { activeElement, showGotoDialog } from '../state'
6
- import Titles from '/@slidev/titles.md'
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(rawRoutes.map(i => i.meta?.slide).filter(notNull), {
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 lang="postcss">
168
+ <style scoped>
169
169
  .autocomplete-list {
170
- @apply bg-main transform mt-1 overflow-auto;
171
- max-height: calc( 100vh - 100px );
170
+ --uno: bg-main mt-1;
171
+ overflow: auto;
172
+ max-height: calc(100vh - 100px);
172
173
  }
173
174
 
174
175
  .autocomplete {
@@ -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
- </button>
12
+ <slot>
13
+ <div :class="icon" />
14
+ </slot>
15
+ </component>
12
16
  </template>
@@ -10,7 +10,7 @@ const props = defineProps({
10
10
  },
11
11
  })
12
12
 
13
- const emit = defineEmits<{ (name: 'modelValue', v: boolean): void }>()
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>
@@ -10,7 +10,7 @@ const props = defineProps({
10
10
  },
11
11
  })
12
12
 
13
- const emit = defineEmits<{ (name: 'modelValue', v: boolean): void }>()
13
+ const emit = defineEmits(['update:modelValue'])
14
14
  const value = useVModel(props, 'modelValue', emit)
15
15
 
16
16
  function onClick() {
@@ -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 { currentPage, downloadPDF, hasNext, hasPrev, isEmbedded, isPresenter, next, presenterPassword, prev, showPresenter, total } from '../logic/nav'
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
- // @ts-expect-error virtual module
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(() => `/presenter/${currentPage.value}${query.value}`)
27
- const nonPresenterLink = computed(() => `/${currentPage.value}${query.value}`)
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-gray-400 dark:border-opacity-10')
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__ && showPresenter" :to="presenterLink" class="slidev-icon-btn" title="Presenter Mode">
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="<md:hidden"
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
- {{ currentPage }}
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
- defineEmits(['click'])
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
- class="prose overflow-auto outline-none"
16
- :class="props.class"
17
- @click="$emit('click')"
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>