@slidev/client 0.40.5 → 0.40.6

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.
@@ -81,9 +81,15 @@ onMounted(() => {
81
81
  line.classList.toggle('dishonored', !highlighted)
82
82
  })
83
83
  if (props.maxHeight) {
84
- const firstHighlightedEl = target.querySelector('.line.highlighted')
85
- if (firstHighlightedEl)
86
- firstHighlightedEl.scrollIntoView({ behavior: 'smooth', block: 'center' })
84
+ const highlightedEls = Array.from(target.querySelectorAll('.line.highlighted'))
85
+ const height = highlightedEls.reduce((acc, el) => el.offsetHeight + acc, 0)
86
+ if (height > el.value.offsetHeight) {
87
+ highlightedEls[0].scrollIntoView({ behavior: 'smooth', block: 'start' })
88
+ }
89
+ else if (highlightedEls.length > 0) {
90
+ const middleEl = highlightedEls[Math.round((highlightedEls.length - 1) / 2)]
91
+ middleEl.scrollIntoView({ behavior: 'smooth', block: 'center' })
92
+ }
87
93
  }
88
94
  }
89
95
  })
@@ -0,0 +1,87 @@
1
+ <script setup lang="ts">
2
+ import { computed, inject, onMounted, onUnmounted, ref, watch } from 'vue'
3
+
4
+ import { injectionClicks, injectionClicksDisabled, injectionClicksElements, injectionRoute, injectionSlideContext, injectionSlidevContext } from '../constants'
5
+
6
+ const props = defineProps<{
7
+ autoPlay?: boolean | 'once' | 'resume' | 'resumeOnce'
8
+ autoPause?: 'slide' | 'click'
9
+ autoReset?: 'slide' | 'click'
10
+ }>()
11
+
12
+ const $slidev = inject(injectionSlidevContext)
13
+ const route = inject(injectionRoute)
14
+ const currentContext = inject(injectionSlideContext)
15
+ const clicks = inject(injectionClicks)
16
+ const clicksDisabled = inject(injectionClicksDisabled)
17
+ const clicksElements = inject(injectionClicksElements)
18
+
19
+ const video = ref<HTMLMediaElement>()
20
+ const played = ref(false)
21
+ const ended = ref(false)
22
+
23
+ const matchRoute = computed(() => {
24
+ if (!video.value || currentContext !== 'slide')
25
+ return false
26
+ return route === $slidev?.nav.currentRoute
27
+ })
28
+
29
+ const matchClick = computed(() => {
30
+ if (!video.value || currentContext !== 'slide' || clicks?.value === undefined || clicksDisabled?.value)
31
+ return false
32
+ return !clicksElements?.value.includes(video.value) || clicksElements?.value[clicks?.value - 1] === video.value
33
+ })
34
+
35
+ const matchRouteAndClick = computed(() => matchRoute.value && matchClick.value)
36
+
37
+ watch(matchRouteAndClick, () => {
38
+ if (!video.value || currentContext !== 'slide')
39
+ return
40
+
41
+ if (matchRouteAndClick.value) {
42
+ if (props.autoReset === 'click')
43
+ video.value.currentTime = 0
44
+ if (props.autoPlay && (!played.value || props.autoPlay === 'resume' || (props.autoPlay === 'resumeOnce' && !ended.value)))
45
+ video.value.play()
46
+ }
47
+
48
+ if ((props.autoPause === 'click' && !matchRouteAndClick.value) || (props.autoPause === 'slide' && !matchRoute.value))
49
+ video.value.pause()
50
+ })
51
+
52
+ watch(matchRoute, () => {
53
+ if (!video.value || currentContext !== 'slide')
54
+ return
55
+
56
+ if (matchRoute.value && props.autoReset === 'slide')
57
+ video.value.currentTime = 0
58
+ })
59
+
60
+ function onPlay() {
61
+ played.value = true
62
+ }
63
+
64
+ function onEnded() {
65
+ ended.value = true
66
+ }
67
+
68
+ onMounted(() => {
69
+ if (!video.value || currentContext !== 'slide')
70
+ return
71
+ video.value?.addEventListener('play', onPlay)
72
+ video.value?.addEventListener('ended', onEnded)
73
+ })
74
+
75
+ onUnmounted(() => {
76
+ if (!video.value || currentContext !== 'slide')
77
+ return
78
+ video.value?.removeEventListener('play', onPlay)
79
+ video.value?.removeEventListener('ended', onEnded)
80
+ })
81
+ </script>
82
+
83
+ <template>
84
+ <video ref="video">
85
+ <slot />
86
+ </video>
87
+ </template>
@@ -31,7 +31,7 @@ const classes = computed(() => {
31
31
  <template>
32
32
  <ol v-if="list && list.length > 0" :class="classes">
33
33
  <li
34
- v-for="item in list"
34
+ v-for="item of list"
35
35
  :key="item.path" class="slidev-toc-item"
36
36
  :class="[{ 'slidev-toc-item-active': item.active }, { 'slidev-toc-item-parent-active': item.activeParent }]"
37
37
  >
@@ -26,7 +26,7 @@ let update: () => void = () => { }
26
26
 
27
27
  document.body.appendChild(styleObject)
28
28
 
29
- const lang = () => {
29
+ function lang() {
30
30
  switch (props.lang) {
31
31
  case 'ts':
32
32
  return 'typescript'
@@ -37,7 +37,7 @@ const lang = () => {
37
37
  }
38
38
  }
39
39
 
40
- const ext = () => {
40
+ function ext() {
41
41
  switch (lang()) {
42
42
  case 'typescript':
43
43
  return 'ts'
@@ -1,74 +1,173 @@
1
1
  <script setup lang="ts">
2
- import { computed, nextTick, ref, watch } from 'vue'
3
- import { go, rawRoutes, total } from '../logic/nav'
4
- import { showGotoDialog } from '../state'
2
+ import { computed, ref, watch } from 'vue'
3
+ import Fuse from 'fuse.js'
4
+ import { go, rawRoutes } from '../logic/nav'
5
+ import { activeElement, showGotoDialog } from '../state'
5
6
 
7
+ const container = ref<HTMLDivElement>()
6
8
  const input = ref<HTMLInputElement>()
9
+ const list = ref<HTMLUListElement>()
10
+ const items = ref<HTMLLIElement[]>()
7
11
  const text = ref('')
12
+ const selectedIndex = ref(0)
8
13
 
9
- const valid = computed(() => {
10
- if (text.value.startsWith('/')) {
11
- return !!rawRoutes.find(r => r.path === text.value.substring(1))
12
- }
13
- else {
14
- const num = +text.value
15
- return !isNaN(num) && num > 0 && num <= total.value
16
- }
17
- })
14
+ function notNull<T>(value: T | null | undefined): value is T {
15
+ return value !== null && value !== undefined
16
+ }
17
+
18
+ const fuse = computed(() => new Fuse(rawRoutes.map(i => i.meta?.slide).filter(notNull), {
19
+ keys: ['no', 'title'],
20
+ threshold: 0.3,
21
+ shouldSort: true,
22
+ minMatchCharLength: 1,
23
+ }))
24
+
25
+ const path = computed(() => text.value.startsWith('/') ? text.value.substring(1) : text.value)
26
+ const result = computed(() => fuse.value.search(path.value).map(result => result.item))
27
+ const valid = computed(() => !!result.value.length)
18
28
 
19
29
  function goTo() {
20
30
  if (valid.value) {
21
- if (text.value.startsWith('/'))
22
- go(text.value.substring(1))
23
-
24
- else
25
- go(+text.value)
31
+ const item = result.value.at(selectedIndex.value || 0)
32
+ if (item)
33
+ go(item.no)
26
34
  }
27
35
  close()
28
36
  }
29
37
 
30
38
  function close() {
39
+ text.value = ''
31
40
  showGotoDialog.value = false
32
41
  }
33
42
 
43
+ function focusDown(event: Event) {
44
+ event.preventDefault()
45
+ selectedIndex.value++
46
+ if (selectedIndex.value >= result.value.length)
47
+ selectedIndex.value = 0
48
+ scroll()
49
+ }
50
+
51
+ function focusUp(event: Event) {
52
+ event.preventDefault()
53
+ selectedIndex.value--
54
+ if (selectedIndex.value <= -2)
55
+ selectedIndex.value = result.value.length - 1
56
+ scroll()
57
+ }
58
+
59
+ function scroll() {
60
+ const item = items.value?.[selectedIndex.value]
61
+ if (item && list.value) {
62
+ if (item.offsetTop + item.offsetHeight > list.value.offsetHeight + list.value.scrollTop) {
63
+ list.value.scrollTo({
64
+ behavior: 'smooth',
65
+ top: item.offsetTop + item.offsetHeight - list.value.offsetHeight + 1,
66
+ })
67
+ }
68
+ else if (item.offsetTop < list.value.scrollTop) {
69
+ list.value.scrollTo({
70
+ behavior: 'smooth',
71
+ top: item.offsetTop,
72
+ })
73
+ }
74
+ }
75
+ }
76
+
77
+ function updateText(event: Event) {
78
+ selectedIndex.value = 0
79
+ text.value = (event.target as HTMLInputElement).value
80
+ }
81
+
82
+ function select(no: number) {
83
+ go(no)
84
+ close()
85
+ }
86
+
34
87
  watch(showGotoDialog, async (show) => {
35
88
  if (show) {
36
- await nextTick()
37
89
  text.value = ''
38
- input.value?.focus()
90
+ selectedIndex.value = 0
91
+ // delay the focus to avoid the g character coming from the key that triggered showGotoDialog
92
+ setTimeout(() => input.value?.focus(), 0)
39
93
  }
40
94
  else {
41
95
  input.value?.blur()
42
96
  }
43
97
  })
44
98
 
45
- // remove the g character coming from the key that triggered showGotoDialog (e.g. in Firefox)
46
- watch(text, (t) => {
47
- if (t.match(/^[^0-9/]/))
48
- text.value = text.value.substring(1)
99
+ watch(activeElement, () => {
100
+ if (!container.value?.contains(activeElement.value as Node))
101
+ close()
49
102
  })
50
103
  </script>
51
104
 
52
105
  <template>
53
106
  <div
54
107
  id="slidev-goto-dialog"
55
- class="fixed right-5 bg-main transform transition-all"
108
+ ref="container"
109
+ class="fixed right-5 transition-all"
56
110
  :class="showGotoDialog ? 'top-5' : '-top-20'"
57
- shadow="~"
58
- p="x-4 y-2"
59
- border="~ transparent rounded dark:gray-400 dark:opacity-25"
60
111
  >
61
- <input
62
- ref="input"
63
- v-model="text"
64
- type="text"
65
- :disabled="!showGotoDialog"
66
- class="outline-none bg-transparent"
67
- placeholder="Goto..."
68
- :class="{ 'text-red-400': !valid && text }"
69
- @keydown.enter="goTo"
70
- @keydown.escape="close"
71
- @blur="close"
112
+ <div
113
+ class="bg-main transform"
114
+ shadow="~"
115
+ p="x-4 y-2"
116
+ border="~ transparent rounded dark:main"
117
+ >
118
+ <input
119
+ ref="input"
120
+ :value="text"
121
+ type="text"
122
+ :disabled="!showGotoDialog"
123
+ class="outline-none bg-transparent"
124
+ placeholder="Goto..."
125
+ :class="{ 'text-red-400': !valid && text }"
126
+ @keydown.enter="goTo"
127
+ @keydown.escape="close"
128
+ @keydown.down="focusDown"
129
+ @keydown.up="focusUp"
130
+ @input="updateText"
131
+ >
132
+ </div>
133
+ <ul
134
+ v-if="result.length > 0"
135
+ ref="list"
136
+ class="autocomplete-list"
137
+ shadow="~"
138
+ border="~ transparent rounded dark:main"
72
139
  >
140
+ <li
141
+ v-for="(item, index) of result"
142
+ ref="items"
143
+ :key="item.id"
144
+ role="button"
145
+ tabindex="0"
146
+ p="x-4 y-2"
147
+ cursor-pointer
148
+ hover="op100"
149
+ flex="~ gap-2"
150
+ items-center
151
+ :border="index === 0 ? '' : 't main'"
152
+ :class="selectedIndex === index ? 'bg-active op100' : 'op80'"
153
+ @click.stop="select(item.no)"
154
+ >
155
+ <div w-4 text-right op50 text-sm>
156
+ {{ item.no }}
157
+ </div>
158
+ {{ item.title }}
159
+ </li>
160
+ </ul>
73
161
  </div>
74
162
  </template>
163
+
164
+ <style scoped lang="postcss">
165
+ .autocomplete-list {
166
+ @apply bg-main transform mt-1 overflow-auto;
167
+ max-height: calc( 100vh - 100px );
168
+ }
169
+
170
+ .autocomplete {
171
+ cursor: pointer;
172
+ }
173
+ </style>
@@ -31,7 +31,7 @@ onClickOutside(el, () => {
31
31
  <div
32
32
  v-if="value"
33
33
  class="rounded-md bg-main shadow absolute bottom-10 left-0 z-20"
34
- dark:border="~ gray-400 opacity-10"
34
+ dark:border="~ main"
35
35
  >
36
36
  <slot name="menu" />
37
37
  </div>
@@ -28,7 +28,7 @@ function onClick() {
28
28
  />
29
29
  <div
30
30
  class="m-auto rounded-md bg-main shadow"
31
- dark:border="~ gray-400 opacity-10"
31
+ dark:border="~ main"
32
32
  :class="props.class"
33
33
  >
34
34
  <slot />
@@ -25,7 +25,7 @@ const presenterLink = computed(() => `/presenter/${currentPage.value}${query.val
25
25
  const nonPresenterLink = computed(() => `/${currentPage.value}${query.value}`)
26
26
 
27
27
  const root = ref<HTMLDivElement>()
28
- const onMouseLeave = () => {
28
+ function onMouseLeave() {
29
29
  if (root.value && activeElement.value && root.value.contains(activeElement.value))
30
30
  activeElement.value.blur()
31
31
  }
@@ -128,6 +128,9 @@ export default function createDirectives() {
128
128
  // Set default dir.value
129
129
  if (dir.value == null)
130
130
  dir.value = elements?.value.length
131
+ // Relative value starts with '+' o '-'
132
+ if (typeof dir.value === 'string' && (dir.value.startsWith('+') || dir.value.startsWith('-')))
133
+ dir.value = (elements?.value?.length || 0) + Number(dir.value)
131
134
 
132
135
  // If a v-click order before v-after is lower than v-after, the order map will
133
136
  // not contain the key for v-after, so we need to set it first, then move v-after
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slidev/client",
3
- "version": "0.40.5",
3
+ "version": "0.40.6",
4
4
  "description": "Presentation slides for developers",
5
5
  "author": "antfu <anthonyfu117@hotmail.com>",
6
6
  "license": "MIT",
@@ -25,13 +25,14 @@
25
25
  "defu": "^6.1.2",
26
26
  "drauu": "^0.3.2",
27
27
  "file-saver": "^2.0.5",
28
+ "fuse.js": "^6.6.2",
28
29
  "js-base64": "^3.7.5",
29
30
  "js-yaml": "^4.1.0",
30
31
  "katex": "^0.16.4",
31
32
  "mermaid": "^9.4.3",
32
33
  "monaco-editor": "^0.33.0",
33
- "nanoid": "^4.0.1",
34
- "prettier": "^2.8.6",
34
+ "nanoid": "^4.0.2",
35
+ "prettier": "^2.8.7",
35
36
  "recordrtc": "^5.6.2",
36
37
  "resolve": "^1.22.1",
37
38
  "unocss": "^0.50.6",
@@ -40,8 +41,8 @@
40
41
  "vue-router": "^4.1.6",
41
42
  "vue-starport": "^0.3.0",
42
43
  "windicss": "^3.5.6",
43
- "@slidev/parser": "0.40.5",
44
- "@slidev/types": "0.40.5"
44
+ "@slidev/parser": "0.40.6",
45
+ "@slidev/types": "0.40.6"
45
46
  },
46
47
  "devDependencies": {
47
48
  "vite": "^4.2.1"
package/routes.ts CHANGED
@@ -78,6 +78,9 @@ declare module 'vue-router' {
78
78
  filepath: string
79
79
  title?: string
80
80
  level?: number
81
+ raw: string
82
+ content: string
83
+ frontmatter: Record<string, any>
81
84
  }
82
85
  transition?: string | TransitionGroupProps | undefined
83
86
  // private fields
package/styles/code.css CHANGED
@@ -12,6 +12,7 @@ html:not(.dark) .shiki-dark {
12
12
 
13
13
  .slidev-code-wrapper {
14
14
  margin: var(--slidev-code-margin) !important;
15
+ scroll-padding: var(--slidev-code-padding);
15
16
  &:-webkit-scrollbar {
16
17
  width: 0px;
17
18
  }
package/uno.config.ts CHANGED
@@ -15,6 +15,8 @@ export default defineConfig({
15
15
  ],
16
16
  shortcuts: {
17
17
  'bg-main': 'bg-white text-[#181818] dark:(bg-[#121212] text-[#ddd])',
18
+ 'bg-active': 'bg-gray-400/10',
19
+ 'border-main': 'border-gray-400/20',
18
20
  'abs-tl': 'absolute top-0 left-0',
19
21
  'abs-tr': 'absolute top-0 right-0',
20
22
  'abs-b': 'absolute bottom-0 left-0 right-0',
package/windi.config.ts CHANGED
@@ -48,6 +48,8 @@ export default defineConfig({
48
48
  ],
49
49
  shortcuts: {
50
50
  'bg-main': 'bg-white text-[#181818] dark:(bg-[#121212] text-[#ddd])',
51
+ 'bg-active': 'bg-gray-400/10',
52
+ 'border-main': 'border-gray-400/20',
51
53
  'abs-tl': 'absolute top-0 left-0',
52
54
  'abs-tr': 'absolute top-0 right-0',
53
55
  'abs-b': 'absolute bottom-0 left-0 right-0',