|
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