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>
70 lines
2.2 KiB
TypeScript
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 }
|
|
}
|