返回 slidev
Goto.vue
根目录 / packages / client / internals / Goto.vue
1 <script setup lang="ts">
2 import Fuse from 'fuse.js'
3 import { computed, ref, watch } from 'vue'
4 import TitleRenderer from '#slidev/title-renderer'
5 import { useNav } from '../composables/useNav'
6 import { activeElement, showGotoDialog } from '../state'
7
8 const container = ref<HTMLDivElement>()
9 const input = ref<HTMLInputElement>()
10 const list = ref<HTMLUListElement>()
11 const items = ref<HTMLLIElement[]>()
12 const text = ref('')
13 const selectedIndex = ref(0)
14
15 const { go, slides } = useNav()
16
17 function notNull<T>(value: T | null | undefined): value is T {
18 return value !== null && value !== undefined
19 }
20
21 const fuse = computed(() => new Fuse(slides.value.map(i => i.meta?.slide).filter(notNull), {
22 keys: ['no', 'title'],
23 threshold: 0.3,
24 shouldSort: true,
25 minMatchCharLength: 1,
26 }))
27
28 const path = computed(() => text.value.startsWith('/') ? text.value.substring(1) : text.value)
29 const result = computed(() => path.value ? fuse.value.search(path.value).map(result => result.item) : [])
30 const valid = computed(() => !!result.value.length)
31
32 function goTo() {
33 if (valid.value) {
34 const item = result.value.at(selectedIndex.value || 0)
35 if (item)
36 go(item.no)
37 }
38 close()
39 }
40
41 function close() {
42 text.value = ''
43 showGotoDialog.value = false
44 }
45
46 function focusDown(event: Event) {
47 event.preventDefault()
48 selectedIndex.value++
49 if (selectedIndex.value >= result.value.length)
50 selectedIndex.value = 0
51 scroll()
52 }
53
54 function focusUp(event: Event) {
55 event.preventDefault()
56 selectedIndex.value--
57 if (selectedIndex.value <= -2)
58 selectedIndex.value = result.value.length - 1
59 scroll()
60 }
61
62 function scroll() {
63 const item = items.value?.[selectedIndex.value]
64 if (item && list.value) {
65 if (item.offsetTop + item.offsetHeight > list.value.offsetHeight + list.value.scrollTop) {
66 list.value.scrollTo({
67 behavior: 'smooth',
68 top: item.offsetTop + item.offsetHeight - list.value.offsetHeight + 1,
69 })
70 }
71 else if (item.offsetTop < list.value.scrollTop) {
72 list.value.scrollTo({
73 behavior: 'smooth',
74 top: item.offsetTop,
75 })
76 }
77 }
78 }
79
80 function updateText(event: Event) {
81 selectedIndex.value = 0
82 text.value = (event.target as HTMLInputElement).value
83 }
84
85 function select(no: number) {
86 go(no)
87 close()
88 }
89
90 watch(showGotoDialog, async (show) => {
91 if (show) {
92 text.value = ''
93 selectedIndex.value = 0
94 // delay the focus to avoid the g character coming from the key that triggered showGotoDialog
95 setTimeout(() => input.value?.focus(), 0)
96 }
97 else {
98 input.value?.blur()
99 }
100 })
101
102 watch(activeElement, () => {
103 if (!container.value?.contains(activeElement.value as Node))
104 close()
105 })
106 </script>
107
108 <template>
109 <div
110 id="slidev-goto-dialog"
111 ref="container"
112 class="fixed right-5 transition-all"
113 w-90 max-w-90 min-w-90
114 :class="showGotoDialog ? 'top-5' : '-top-20 op0 pointer-events-none'"
115 >
116 <div
117 class="bg-main transform"
118 shadow="~"
119 p="x-4 y-2"
120 border="~ transparent rounded dark:main"
121 >
122 <input
123 id="slidev-goto-input"
124 ref="input"
125 :value="text"
126 type="text"
127 :disabled="!showGotoDialog"
128 class="outline-none bg-transparent"
129 placeholder="Goto..."
130 :class="{ 'text-red-400': !valid && text }"
131 @keydown.enter="goTo"
132 @keydown.escape="close"
133 @keydown.down="focusDown"
134 @keydown.up="focusUp"
135 @input="updateText"
136 >
137 </div>
138 <div
139 v-if="showGotoDialog && result.length > 0"
140 ref="list"
141 class="autocomplete-list"
142 shadow="~"
143 border="~ transparent rounded dark:main"
144 >
145 <ul table w-full border-collapse>
146 <li
147 v-for="(item, index) of result"
148 ref="items"
149 :key="item.id"
150 role="button"
151 tabindex="0"
152 cursor-pointer
153 hover="op100"
154 table-row
155 items-center
156 :border="index === 0 ? undefined : 't main'"
157 :class="selectedIndex === index ? 'bg-active op100' : 'op80'"
158 @click.stop.prevent="select(item.no)"
159 >
160 <div text-right op50 text-sm table-cell py-2 pl-4 pr-3 vertical-middle>
161 {{ item.no }}
162 </div>
163 <TitleRenderer table-cell py-2 pr-4 w-full :no="item.no" />
164 </li>
165 </ul>
166 </div>
167 </div>
168 </template>
169
170 <style scoped>
171 .autocomplete-list {
172 --uno: bg-main mt-1;
173 overflow: auto;
174 max-height: calc(100vh - 100px);
175 }
176
177 .autocomplete {
178 cursor: pointer;
179 }
180 </style>
181
181 lines Plain Text