|
1 | 1 | <script setup lang="ts">
|
2 |
| -import { useEventListener } from '@vueuse/core' |
| 2 | +import { useEventListener, useVirtualList } from '@vueuse/core' |
3 | 3 | import { computed, ref, watchEffect } from 'vue'
|
4 | 4 | import { breakpoints, showOverview, windowSize } from '../state'
|
5 | 5 | import { currentOverviewPage, overviewRowCount } from '../logic/overview'
|
6 | 6 | import { createFixedClicks } from '../composables/useClicks'
|
7 | 7 | import { CLICKS_MAX } from '../constants'
|
8 | 8 | import { useNav } from '../composables/useNav'
|
| 9 | +import { slideAspect } from '../env' |
9 | 10 | import SlideContainer from './SlideContainer.vue'
|
10 | 11 | import SlideWrapper from './SlideWrapper.vue'
|
11 | 12 | import DrawingPreview from './DrawingPreview.vue'
|
@@ -41,10 +42,29 @@ const cardWidth = computed(() => {
|
41 | 42 | return 300
|
42 | 43 | })
|
43 | 44 |
|
| 45 | +const cardHeight = computed(() => cardWidth.value / slideAspect.value) |
| 46 | +
|
44 | 47 | const rowCount = computed(() => {
|
45 |
| - return Math.floor((windowSize.width.value - padding) / (cardWidth.value + gap)) |
| 48 | + return Math.floor((windowSize.width.value - padding) / (cardWidth.value + 2 * gap)) |
| 49 | +}) |
| 50 | +
|
| 51 | +const numOfRows = computed(() => { |
| 52 | + return Math.ceil(slides.value.length / rowCount.value) |
| 53 | +}) |
| 54 | +
|
| 55 | +const slideRows = computed(() => { |
| 56 | + return [...Array(numOfRows.value)].map((_, i) => ({ rowIdx: i, slides: slides.value.slice(i * rowCount.value, (i + 1) * rowCount.value).map((route, j) => ({ route, idx: i * rowCount.value + j })) })) |
46 | 57 | })
|
47 | 58 |
|
| 59 | +const { list: vList, containerProps, wrapperProps } = useVirtualList( |
| 60 | + slideRows, |
| 61 | + { |
| 62 | + itemHeight: cardHeight.value + gap, |
| 63 | + itemWidth: (cardWidth.value + gap) * rowCount.value, |
| 64 | + overscan: 3, |
| 65 | + }, |
| 66 | +) |
| 67 | +
|
48 | 68 | const keyboardBuffer = ref<string>('')
|
49 | 69 |
|
50 | 70 | useEventListener('keypress', (e) => {
|
@@ -114,47 +134,51 @@ setTimeout(() => {
|
114 | 134 | <div
|
115 | 135 | v-if="showOverview || activeSlidesLoaded"
|
116 | 136 | v-show="showOverview"
|
| 137 | + v-bind="containerProps" |
117 | 138 | class="fixed left-0 right-0 top-0 h-[calc(var(--vh,1vh)*100)] z-20 bg-main !bg-opacity-75 p-16 py-20 overflow-y-auto backdrop-blur-5px"
|
118 | 139 | @click="close"
|
119 | 140 | >
|
120 |
| - <div |
121 |
| - class="grid gap-y-4 gap-x-8 w-full" |
122 |
| - :style="`grid-template-columns: repeat(auto-fit,minmax(${cardWidth}px,1fr))`" |
123 |
| - > |
| 141 | + <div v-bind="wrapperProps"> |
124 | 142 | <div
|
125 |
| - v-for="(route, idx) of slides" |
126 |
| - :key="route.no" |
127 |
| - class="relative" |
| 143 | + v-for="{ data: { rowIdx, slides: row } } of vList" |
| 144 | + :key="rowIdx" |
| 145 | + class="flex mb-8" |
128 | 146 | >
|
129 | 147 | <div
|
130 |
| - class="inline-block border rounded overflow-hidden bg-main hover:border-primary transition" |
131 |
| - :class="(focus(idx + 1) || currentOverviewPage === idx + 1) ? 'border-primary' : 'border-main'" |
132 |
| - @click="go(route.no)" |
| 148 | + v-for="{ route, idx } of row" |
| 149 | + :key="route.no" |
| 150 | + class="relative flex-1" |
133 | 151 | >
|
134 |
| - <SlideContainer |
135 |
| - :key="route.no" |
136 |
| - :width="cardWidth" |
137 |
| - class="pointer-events-none" |
| 152 | + <div |
| 153 | + class="inline-block border rounded overflow-hidden bg-main hover:border-primary transition" |
| 154 | + :class="(focus(idx + 1) || currentOverviewPage === idx + 1) ? 'border-primary' : 'border-main'" |
| 155 | + @click="go(route.no)" |
138 | 156 | >
|
139 |
| - <SlideWrapper |
140 |
| - :clicks-context="createFixedClicks(route, CLICKS_MAX)" |
141 |
| - :route="route" |
142 |
| - render-context="overview" |
143 |
| - /> |
144 |
| - <DrawingPreview :page="route.no" /> |
145 |
| - </SlideContainer> |
146 |
| - </div> |
147 |
| - <div |
148 |
| - class="absolute top-0" |
149 |
| - :style="`left: ${cardWidth + 5}px`" |
150 |
| - > |
151 |
| - <template v-if="keyboardBuffer && String(idx + 1).startsWith(keyboardBuffer)"> |
152 |
| - <span class="text-green font-bold">{{ keyboardBuffer }}</span> |
153 |
| - <span class="opacity-50">{{ String(idx + 1).slice(keyboardBuffer.length) }}</span> |
154 |
| - </template> |
155 |
| - <span v-else class="opacity-50"> |
156 |
| - {{ idx + 1 }} |
157 |
| - </span> |
| 157 | + <SlideContainer |
| 158 | + :key="route.no" |
| 159 | + :width="cardWidth" |
| 160 | + class="pointer-events-none" |
| 161 | + > |
| 162 | + <SlideWrapper |
| 163 | + :clicks-context="createFixedClicks(route, CLICKS_MAX)" |
| 164 | + :route="route" |
| 165 | + render-context="overview" |
| 166 | + /> |
| 167 | + <DrawingPreview :page="route.no" /> |
| 168 | + </SlideContainer> |
| 169 | + </div> |
| 170 | + <div |
| 171 | + class="absolute top-0" |
| 172 | + :style="`left: ${cardWidth + 5}px`" |
| 173 | + > |
| 174 | + <template v-if="keyboardBuffer && String(idx + 1).startsWith(keyboardBuffer)"> |
| 175 | + <span class="text-green font-bold">{{ keyboardBuffer }}</span> |
| 176 | + <span class="opacity-50">{{ String(idx + 1).slice(keyboardBuffer.length) }}</span> |
| 177 | + </template> |
| 178 | + <span v-else class="opacity-50"> |
| 179 | + {{ idx + 1 }} |
| 180 | + </span> |
| 181 | + </div> |
158 | 182 | </div>
|
159 | 183 | </div>
|
160 | 184 | </div>
|
|
0 commit comments