Skip to content

Commit 5ce2ec1

Browse files
committed
Optimize /overview page with virtual scroll
1 parent de0e93f commit 5ce2ec1

File tree

1 file changed

+87
-78
lines changed

1 file changed

+87
-78
lines changed

packages/client/pages/overview.vue

Lines changed: 87 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
import { computed, nextTick, onMounted, reactive, ref } from 'vue'
33
import { useHead } from '@unhead/vue'
44
import type { ClicksContext, SlideRoute } from '@slidev/types'
5-
import { slidesTitle } from '../env'
5+
import { useVirtualList } from '@vueuse/core'
6+
import { slideAspect, slidesTitle } from '../env'
67
import { getSlidePath } from '../logic/slides'
78
import { createFixedClicks } from '../composables/useClicks'
89
import { isColorSchemaConfigured, isDark, toggleDark } from '../logic/dark'
@@ -17,6 +18,8 @@ import { useNav } from '../composables/useNav'
1718
1819
const cardWidth = 450
1920
21+
const cardHeight = computed(() => cardWidth / slideAspect.value)
22+
2023
useHead({ title: `Overview - ${slidesTitle}` })
2124
2225
const { openInEditor, slides } = useNav()
@@ -28,6 +31,13 @@ const wordCounts = computed(() => slides.value.map(route => wordCount(route.meta
2831
const totalWords = computed(() => wordCounts.value.reduce((a, b) => a + b, 0))
2932
const totalClicks = computed(() => slides.value.map(route => getSlideClicks(route)).reduce((a, b) => a + b, 0))
3033
34+
const { list: vList, containerProps, wrapperProps, scrollTo: scrollToSlide } = useVirtualList(
35+
slides,
36+
{
37+
itemHeight: cardHeight.value,
38+
},
39+
)
40+
3141
const clicksContextMap = new WeakMap<SlideRoute, ClicksContext>()
3242
function getClicksContext(route: SlideRoute) {
3343
// We create a local clicks context to calculate the total clicks of the slide
@@ -45,6 +55,8 @@ function wordCount(str: string) {
4555
}
4656
4757
function isElementInViewport(el: HTMLElement) {
58+
if (!el)
59+
return false
4860
const rect = el.getBoundingClientRect()
4961
const delta = 20
5062
return (
@@ -72,12 +84,6 @@ function openSlideInNewTab(path: string) {
7284
a.click()
7385
}
7486
75-
function scrollToSlide(idx: number) {
76-
const el = blocks.get(idx)
77-
if (el)
78-
el.scrollIntoView({ behavior: 'smooth', block: 'start' })
79-
}
80-
8187
function onMarkerClick(e: MouseEvent, clicks: number, route: SlideRoute) {
8288
const ctx = getClicksContext(route)
8389
if (ctx.current === clicks)
@@ -136,85 +142,88 @@ onMounted(() => {
136142
<main
137143
class="flex-1 h-full of-auto"
138144
:style="`grid-template-columns: repeat(auto-fit,minmax(${cardWidth}px,1fr))`"
145+
v-bind="containerProps"
139146
@scroll="checkActiveBlocks"
140147
>
141-
<div
142-
v-for="(route, idx) of slides"
143-
:key="route.no"
144-
:ref="el => blocks.set(idx, el as any)"
145-
class="relative border-t border-main of-hidden flex gap-4 min-h-50 group"
146-
:class="idx === 0 ? 'pt5' : ''"
147-
>
148-
<div class="select-none w-13 text-right my4 flex flex-col gap-1 items-end">
149-
<div class="text-3xl op20 mb2">
150-
{{ idx + 1 }}
148+
<div v-bind="wrapperProps">
149+
<div
150+
v-for="{ data: route, index: idx } of vList"
151+
:key="route.no"
152+
:ref="el => blocks.set(idx, el as any)"
153+
class="relative border-t border-main of-hidden flex gap-4 min-h-50 group"
154+
:class="idx === 0 ? 'pt5' : ''"
155+
>
156+
<div class="select-none w-13 text-right my4 flex flex-col gap-1 items-end">
157+
<div class="text-3xl op20 mb2">
158+
{{ idx + 1 }}
159+
</div>
160+
<IconButton
161+
class="mr--3 op0 group-hover:op80"
162+
title="Play in new tab"
163+
@click="openSlideInNewTab(getSlidePath(route, false))"
164+
>
165+
<carbon:presentation-file />
166+
</IconButton>
167+
<IconButton
168+
v-if="route.meta?.slide"
169+
class="mr--3 op0 group-hover:op80"
170+
title="Open in editor"
171+
@click="openInEditor(`${route.meta.slide.filepath}:${route.meta.slide.start}`)"
172+
>
173+
<carbon:cics-program />
174+
</IconButton>
151175
</div>
152-
<IconButton
153-
class="mr--3 op0 group-hover:op80"
154-
title="Play in new tab"
155-
@click="openSlideInNewTab(getSlidePath(route, false))"
156-
>
157-
<carbon:presentation-file />
158-
</IconButton>
159-
<IconButton
160-
v-if="route.meta?.slide"
161-
class="mr--3 op0 group-hover:op80"
162-
title="Open in editor"
163-
@click="openInEditor(`${route.meta.slide.filepath}:${route.meta.slide.start}`)"
164-
>
165-
<carbon:cics-program />
166-
</IconButton>
167-
</div>
168-
<div class="flex flex-col gap-2 my5">
169-
<div
170-
class="border rounded border-main overflow-hidden bg-main select-none h-max"
171-
@dblclick="openSlideInNewTab(getSlidePath(route, false))"
172-
>
173-
<SlideContainer
174-
:key="route.no"
175-
:width="cardWidth"
176-
class="pointer-events-none important:[&_*]:select-none"
176+
<div class="flex flex-col gap-2 my5">
177+
<div
178+
class="border rounded border-main overflow-hidden bg-main select-none h-max"
179+
@dblclick="openSlideInNewTab(getSlidePath(route, false))"
180+
>
181+
<SlideContainer
182+
:key="route.no"
183+
:width="cardWidth"
184+
class="pointer-events-none important:[&_*]:select-none"
185+
>
186+
<SlideWrapper
187+
:clicks-context="getClicksContext(route)"
188+
:route="route"
189+
render-context="overview"
190+
/>
191+
<DrawingPreview :page="route.no" />
192+
</SlideContainer>
193+
</div>
194+
<ClicksSlider
195+
v-if="getSlideClicks(route)"
196+
:clicks-context="getClicksContext(route)"
197+
class="w-full mt-2"
198+
@dblclick="getClicksContext(route).current = CLICKS_MAX"
199+
/>
200+
</div>
201+
<div class="py3 mt-0.5 mr--8 ml--4 op0 transition group-hover:op100">
202+
<IconButton
203+
title="Edit Note"
204+
class="rounded-full w-9 h-9 text-sm"
205+
:class="edittingNote === route.no ? 'important:op0' : ''"
206+
@click="edittingNote = route.no"
177207
>
178-
<SlideWrapper
179-
:clicks-context="getClicksContext(route)"
180-
:route="route"
181-
render-context="overview"
182-
/>
183-
<DrawingPreview :page="route.no" />
184-
</SlideContainer>
208+
<carbon:pen />
209+
</IconButton>
185210
</div>
186-
<ClicksSlider
187-
v-if="getSlideClicks(route)"
211+
<NoteEditable
212+
:no="route.no"
213+
class="max-w-250 w-250 text-lg rounded p3"
214+
:auto-height="true"
215+
:editing="edittingNote === route.no"
188216
:clicks-context="getClicksContext(route)"
189-
class="w-full mt-2"
190-
@dblclick="getClicksContext(route).current = CLICKS_MAX"
217+
@dblclick="edittingNote !== route.no ? edittingNote = route.no : null"
218+
@update:editing="edittingNote = null"
219+
@marker-click="(e, clicks) => onMarkerClick(e, clicks, route)"
191220
/>
192-
</div>
193-
<div class="py3 mt-0.5 mr--8 ml--4 op0 transition group-hover:op100">
194-
<IconButton
195-
title="Edit Note"
196-
class="rounded-full w-9 h-9 text-sm"
197-
:class="edittingNote === route.no ? 'important:op0' : ''"
198-
@click="edittingNote = route.no"
221+
<div
222+
v-if="wordCounts[idx] > 0"
223+
class="select-none absolute bottom-0 right-0 bg-main rounded-tl p2 op35 text-xs"
199224
>
200-
<carbon:pen />
201-
</IconButton>
202-
</div>
203-
<NoteEditable
204-
:no="route.no"
205-
class="max-w-250 w-250 text-lg rounded p3"
206-
:auto-height="true"
207-
:editing="edittingNote === route.no"
208-
:clicks-context="getClicksContext(route)"
209-
@dblclick="edittingNote !== route.no ? edittingNote = route.no : null"
210-
@update:editing="edittingNote = null"
211-
@marker-click="(e, clicks) => onMarkerClick(e, clicks, route)"
212-
/>
213-
<div
214-
v-if="wordCounts[idx] > 0"
215-
class="select-none absolute bottom-0 right-0 bg-main rounded-tl p2 op35 text-xs"
216-
>
217-
{{ wordCounts[idx] }} words
225+
{{ wordCounts[idx] }} words
226+
</div>
218227
</div>
219228
</div>
220229
</main>

0 commit comments

Comments
 (0)