Skip to content
Draft
30 changes: 30 additions & 0 deletions packages/client/composables/useDynamicVirtualList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { UseVirtualListOptions } from '@vueuse/core'
import { debouncedWatch, useVirtualList } from '@vueuse/core'
import type { MaybeRef } from 'vue'
import { effectScope, shallowRef } from 'vue'

/**
* `useVirtualList`'s `itemHeight` is not reactive, so we need to re-create the virtual list when the card height changes.
*/
export function useDynamicVirtualList<T>(list: MaybeRef<T[]>, getOptions: () => UseVirtualListOptions) {
type VirtualListReturn = ReturnType<typeof useVirtualList<T>>
const virtualList = shallowRef<VirtualListReturn>()
debouncedWatch(
getOptions,
(options, _oldOptions, onCleanup) => {
const scope = effectScope()
scope.run(() => {
virtualList.value = useVirtualList(
list,
options,
)
})
onCleanup(() => scope.stop())
},
{
immediate: true,
debounce: 50,
},
)
return virtualList
}
129 changes: 76 additions & 53 deletions packages/client/internals/QuickOverview.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
<script setup lang="ts">
import { useEventListener } from '@vueuse/core'
import { useElementSize, useEventListener } from '@vueuse/core'
import { computed, ref, watchEffect } from 'vue'
import { breakpoints, showOverview, windowSize } from '../state'
import type { SlideRoute } from '@slidev/types'
import { breakpoints, showOverview } from '../state'
import { currentOverviewPage, overviewRowCount } from '../logic/overview'
import { createFixedClicks } from '../composables/useClicks'
import { CLICKS_MAX } from '../constants'
import { useNav } from '../composables/useNav'
import { slideAspect } from '../env'
import { useDynamicVirtualList } from '../composables/useDynamicVirtualList'
import SlideContainer from './SlideContainer.vue'
import SlideWrapper from './SlideWrapper.vue'
import DrawingPreview from './DrawingPreview.vue'
Expand All @@ -22,29 +25,44 @@ function go(page: number) {
close()
}

function focus(page: number) {
if (page === currentOverviewPage.value)
return true
return false
}

const xs = breakpoints.smaller('xs')
const sm = breakpoints.smaller('sm')

const padding = 4 * 16 * 2
const gap = 2 * 16
const gapX = 2 * 16
const gapY = 4 * 8 // mb-8

const containerEl = ref<HTMLElement>()
const { width: containerWidth } = useElementSize(containerEl)

const cardWidth = computed(() => {
if (xs.value)
return windowSize.width.value - padding
else if (sm.value)
return (windowSize.width.value - padding - gap) / 2
return 300
return xs.value
? containerWidth.value
: Math.min(300, (containerWidth.value - gapX) / 2)
})

const numOfCols = computed(() => {
return xs.value
? 1
: Math.floor((containerWidth.value + gapX) / (cardWidth.value + gapX))
})

const cardHeight = computed(() => cardWidth.value / slideAspect.value)

const numOfRows = computed(() => {
return Math.ceil(slides.value.length / numOfCols.value)
})

const rowCount = computed(() => {
return Math.floor((windowSize.width.value - padding) / (cardWidth.value + gap))
const slideRows = computed(() => {
const cols = numOfCols.value
const rows: SlideRoute[][] = []
for (let i = 0; i < numOfRows.value; i++)
rows.push(slides.value.slice(i * cols, (i + 1) * cols))
return rows
})

const virtualList = useDynamicVirtualList(slideRows, () => ({
itemHeight: cardHeight.value + gapY,
}))

const keyboardBuffer = ref<string>('')

useEventListener('keypress', (e) => {
Expand Down Expand Up @@ -95,7 +113,7 @@ watchEffect(() => {
// we focus on the right page.
currentOverviewPage.value = currentSlideNo.value
// Watch rowCount, make sure up and down shortcut work correctly.
overviewRowCount.value = rowCount.value
overviewRowCount.value = numOfCols.value
})

const activeSlidesLoaded = ref(false)
Expand All @@ -114,47 +132,52 @@ setTimeout(() => {
<div
v-if="showOverview || activeSlidesLoaded"
v-show="showOverview"
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"
v-bind="virtualList?.containerProps"
class="fixed left-0 right-0 top-0 h-[calc(var(--vh,1vh)*100)] z-20 bg-main !bg-opacity-75 px-16 py-20 overflow-y-auto backdrop-blur-5px"
@click="close"
>
<div
class="grid gap-y-4 gap-x-8 w-full"
:style="`grid-template-columns: repeat(auto-fit,minmax(${cardWidth}px,1fr))`"
>
<div ref="containerEl" v-bind="virtualList?.wrapperProps.value">
<div
v-for="(route, idx) of slides"
:key="route.no"
class="relative"
v-for="{ index: rowIdx, data: row } of virtualList?.list.value"
:key="rowIdx"
class="grid grid-rows-1 gap-x-8 w-full mb-8"
:style="`grid-template-columns: repeat(auto-fit,minmax(${cardWidth}px,1fr))`"
>
<div
class="inline-block border rounded overflow-hidden bg-main hover:border-primary transition"
:class="(focus(idx + 1) || currentOverviewPage === idx + 1) ? 'border-primary' : 'border-main'"
@click="go(route.no)"
v-for="route of row"
:key="route.no"
class="relative"
>
<SlideContainer
:key="route.no"
:width="cardWidth"
class="pointer-events-none"
<div
class="inline-block border rounded overflow-hidden bg-main hover:border-primary transition"
:class="currentOverviewPage === route.no ? 'border-primary' : 'border-main'"
@click="go(route.no)"
>
<SlideWrapper
:clicks-context="createFixedClicks(route, CLICKS_MAX)"
:route="route"
render-context="overview"
/>
<DrawingPreview :page="route.no" />
</SlideContainer>
</div>
<div
class="absolute top-0"
:style="`left: ${cardWidth + 5}px`"
>
<template v-if="keyboardBuffer && String(idx + 1).startsWith(keyboardBuffer)">
<span class="text-green font-bold">{{ keyboardBuffer }}</span>
<span class="opacity-50">{{ String(idx + 1).slice(keyboardBuffer.length) }}</span>
</template>
<span v-else class="opacity-50">
{{ idx + 1 }}
</span>
<SlideContainer
:key="route.no"
:width="cardWidth"
class="pointer-events-none"
>
<SlideWrapper
:clicks-context="createFixedClicks(route, CLICKS_MAX)"
:route="route"
render-context="overview"
/>
<DrawingPreview :page="route.no" />
</SlideContainer>
</div>
<div
class="absolute top-0"
:style="`left: ${cardWidth + 5}px`"
>
<template v-if="keyboardBuffer && String(route.no).startsWith(keyboardBuffer)">
<span class="text-green font-bold">{{ keyboardBuffer }}</span>
<span class="opacity-50">{{ String(route.no).slice(keyboardBuffer.length) }}</span>
</template>
<span v-else class="opacity-50">
{{ route.no }}
</span>
</div>
</div>
</div>
</div>
Expand Down
Loading