Files
portfolio-generator-app/composables/usePageScrollSpy.ts
Valentin Le Moign 59dfbe4155 Générateur de portfolios PDF Figures Libres (app Nuxt autonome)
App Nuxt 3 + Nuxt UI qui lit les projets du CMS Grav et compose des portfolios
A3 paysage exportés en PDF (gabarit book_v3) : composeur WYSIWYG, templates
cover/toc/projet/grille/page libre, pipeline Playwright + Ghostscript, gel du
contenu à la génération, partage public opt-in.

Dépôt autonome : Dockerfile (base Playwright + Ghostscript + ffmpeg + rsync) inclus.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 18:37:09 +02:00

70 lines
2.2 KiB
TypeScript

import type { Ref } from 'vue'
/**
* Scroll-spy des pages empilées : la page active suit en temps réel la page au
* centre du conteneur défilable, et permet de défiler vers une page donnée.
* Partagé par le composeur (pages/portfolios/[id].vue) et la vue publique
* (pages/view/[id].vue) — chacun câble sa propre ref `active` et son `idPrefix`.
*
* - `container` : élément défilable (l'aperçu).
* - `pages` : liste courante des pages (on lit `.id` et l'ordre).
* - `active` : ref d'id écrite à chaque défilement (jamais de re-scroll).
* - `idPrefix` : préfixe des id DOM des pages (`preview-page` / `view-page`),
* suffixés par la position 1-based.
*/
export function usePageScrollSpy(options: {
container: Ref<HTMLElement | undefined>
pages: Ref<{ id: string }[]>
active: Ref<string | undefined>
idPrefix: string
}) {
const { container, pages, active, idPrefix } = options
function elementFor(index: number): HTMLElement | null {
return document.getElementById(`${idPrefix}-${index + 1}`)
}
function scrollToIndex(i: number) {
nextTick(() => {
elementFor(i)?.scrollIntoView({ behavior: 'smooth', block: 'start' })
})
}
function scrollToId(id: string) {
const i = pages.value.findIndex(p => p.id === id)
if (i !== -1) scrollToIndex(i)
}
// Une mise à jour par frame (rAF) pour rester fluide sans recalcul redondant.
let rafPending = false
function onScroll() {
if (rafPending) return
rafPending = true
requestAnimationFrame(() => {
rafPending = false
updateActiveFromScroll()
})
}
function updateActiveFromScroll() {
const cont = container.value
if (!cont) return
const mid = cont.getBoundingClientRect().top + cont.clientHeight / 2
let bestId: string | undefined
let bestDist = Infinity
pages.value.forEach((page, i) => {
const el = elementFor(i)
if (!el) return
const r = el.getBoundingClientRect()
const dist = Math.abs(r.top + r.height / 2 - mid)
if (dist < bestDist) {
bestDist = dist
bestId = page.id
}
})
if (bestId && bestId !== active.value) active.value = bestId
}
return { onScroll, scrollToIndex, scrollToId }
}