mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-08-10 15:21:22 +02:00
Fix lint: use IterableCallbackReturn — wrap forEach body in braces
This commit is contained in:
@@ -0,0 +1,368 @@
|
||||
/**
|
||||
* pdf-viewer.js — In-page PDF.js viewer (no download UI, scroll isolation).
|
||||
*
|
||||
* Two states per PDF container:
|
||||
* Collapsed — thumbnail preview tile, blurred veil, "Lire" button.
|
||||
* Outer page scrolls freely.
|
||||
* Expanded — the wrapper itself becomes the scrollable panel.
|
||||
* Toolbar + pages. "Fermer" button in toolbar.
|
||||
*
|
||||
* Dependencies: pdfjsLib (from vendor/pdf.min.mjs), pdf.worker (from
|
||||
* vendor/pdf.worker.min.mjs).
|
||||
*/
|
||||
|
||||
(() => {
|
||||
function bootstrap() {
|
||||
const containers = document.querySelectorAll(".pdf-embed-wrapper");
|
||||
if (containers.length === 0) return;
|
||||
|
||||
if (typeof pdfjsLib === "undefined") {
|
||||
setTimeout(bootstrap, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!pdfjsLib.GlobalWorkerOptions.workerSrc) {
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc =
|
||||
"/assets/js/vendor/pdf.worker.min.mjs";
|
||||
}
|
||||
|
||||
for (const c of containers) initPdfViewer(c);
|
||||
}
|
||||
|
||||
// ── Init ──────────────────────────────────────────────────────
|
||||
|
||||
async function initPdfViewer(container) {
|
||||
const url = container.dataset.pdfUrl;
|
||||
if (!url) return;
|
||||
|
||||
// Ensure URL is a non-empty string
|
||||
if (typeof url !== "string" || url.trim() === "") {
|
||||
container.textContent = "URL du PDF invalide.";
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const pdfDoc = await pdfjsLib.getDocument({ url }).promise;
|
||||
container._pdfDoc = pdfDoc;
|
||||
container._numPages = pdfDoc.numPages;
|
||||
|
||||
buildCollapsed(container, pdfDoc);
|
||||
} catch (err) {
|
||||
container.textContent = "PDF non disponible.";
|
||||
console.error("PDF.js error:", err);
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// COLLAPSED STATE — thumbnail tile
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
async function buildCollapsed(container, pdfDoc) {
|
||||
container.classList.add("pdf-collapsed");
|
||||
|
||||
// Render first page as a thumbnail
|
||||
try {
|
||||
const page = await pdfDoc.getPage(1);
|
||||
const viewport = page.getViewport({ scale: 0.3 });
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.className = "pdf-thumbnail";
|
||||
canvas.width = Math.floor(viewport.width * dpr);
|
||||
canvas.height = Math.floor(viewport.height * dpr);
|
||||
canvas.style.width = `${Math.floor(viewport.width)}px`;
|
||||
canvas.style.height = `${Math.floor(viewport.height)}px`;
|
||||
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
await page.render({ canvasContext: ctx, viewport }).promise;
|
||||
|
||||
container.appendChild(canvas);
|
||||
} catch {
|
||||
// Thumbnail optional
|
||||
}
|
||||
|
||||
// Veil overlay
|
||||
const overlay = document.createElement("div");
|
||||
overlay.className = "pdf-overlay";
|
||||
container.appendChild(overlay);
|
||||
|
||||
// Document type pill — top-left corner of the overlay
|
||||
const label = container.dataset.pdfLabel;
|
||||
if (label) {
|
||||
const pill = document.createElement("span");
|
||||
pill.className = "pdf-overlay-badge";
|
||||
pill.textContent = label;
|
||||
overlay.appendChild(pill);
|
||||
}
|
||||
|
||||
// "Lire" button
|
||||
const btn = document.createElement("button");
|
||||
btn.className = "pdf-enter-btn";
|
||||
btn.textContent = "Lire le document";
|
||||
btn.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
expand(container, pdfDoc);
|
||||
});
|
||||
overlay.appendChild(btn);
|
||||
|
||||
overlay.addEventListener("click", () => expand(container, pdfDoc));
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// EXPANDED STATE — wrapper becomes the scroll panel
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
async function expand(container, pdfDoc) {
|
||||
container.classList.remove("pdf-collapsed");
|
||||
container.classList.add("pdf-expanded");
|
||||
|
||||
container.addEventListener("contextmenu", (e) => e.preventDefault());
|
||||
container.addEventListener("keydown", (e) => {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "s") {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
container.setAttribute("tabindex", "-1");
|
||||
|
||||
// Remove collapsed elements
|
||||
const oldOverlay = container.querySelector(".pdf-overlay");
|
||||
if (oldOverlay) oldOverlay.remove();
|
||||
const oldThumb = container.querySelector(".pdf-thumbnail");
|
||||
if (oldThumb) oldThumb.remove();
|
||||
|
||||
// Build toolbar
|
||||
buildToolbar(container, pdfDoc);
|
||||
|
||||
// Render pages directly into the wrapper
|
||||
await renderAllPages(container, pdfDoc);
|
||||
|
||||
container.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
||||
}
|
||||
|
||||
function collapse(container) {
|
||||
container.classList.remove("pdf-expanded");
|
||||
container.classList.add("pdf-collapsed");
|
||||
|
||||
const oldToolbar = container.querySelector(".pdf-toolbar");
|
||||
if (oldToolbar) oldToolbar.remove();
|
||||
|
||||
// Remove rendered pages
|
||||
container.querySelectorAll(".pdf-canvas").forEach((c) => {
|
||||
c.remove();
|
||||
});
|
||||
|
||||
buildCollapsed(container, container._pdfDoc);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// PAGE RENDERING (canvases appended directly to container)
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
async function renderAllPages(container, pdfDoc) {
|
||||
const numPages = pdfDoc.numPages;
|
||||
|
||||
const firstPage = await pdfDoc.getPage(1);
|
||||
const firstViewport = firstPage.getViewport({ scale: 1 });
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
|
||||
const pageEntries = [];
|
||||
for (let i = 1; i <= numPages; i++) {
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.className = "pdf-canvas";
|
||||
canvas.dataset.page = String(i);
|
||||
container.appendChild(canvas);
|
||||
pageEntries.push({ canvas, pageNum: i });
|
||||
}
|
||||
|
||||
const rendering = new Set();
|
||||
|
||||
const resolveScale = () => {
|
||||
const userScale = parseFloat(container.dataset.pdfScale ?? "1");
|
||||
const containerWidth = container.clientWidth - 16;
|
||||
if (containerWidth <= 0) return userScale;
|
||||
return (containerWidth / firstViewport.width) * userScale;
|
||||
};
|
||||
|
||||
const renderPage = async (entry) => {
|
||||
const { canvas, pageNum } = entry;
|
||||
if (rendering.has(pageNum)) return;
|
||||
rendering.add(pageNum);
|
||||
try {
|
||||
const page = await pdfDoc.getPage(pageNum);
|
||||
const scale = resolveScale();
|
||||
const viewport = page.getViewport({ scale });
|
||||
canvas.width = Math.floor(viewport.width * dpr);
|
||||
canvas.height = Math.floor(viewport.height * dpr);
|
||||
canvas.style.width = `${Math.floor(viewport.width)}px`;
|
||||
canvas.style.height = `${Math.floor(viewport.height)}px`;
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
await page.render({ canvasContext: ctx, viewport }).promise;
|
||||
} catch (err) {
|
||||
if (err.name !== "RenderingCancelledException") {
|
||||
console.error(`Failed to render page ${pageNum}:`, err);
|
||||
}
|
||||
} finally {
|
||||
rendering.delete(pageNum);
|
||||
}
|
||||
};
|
||||
|
||||
const scheduleVisible = () => {
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
for (const entry of pageEntries) {
|
||||
if (rendering.has(entry.pageNum)) continue;
|
||||
const rect = entry.canvas.getBoundingClientRect();
|
||||
const margin = containerRect.height * 2;
|
||||
if (
|
||||
rect.bottom >= containerRect.top - margin &&
|
||||
rect.top <= containerRect.bottom + margin
|
||||
) {
|
||||
renderPage(entry);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
scheduleVisible();
|
||||
container.addEventListener("scroll", scheduleVisible, { passive: true });
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// TOOLBAR
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
function buildToolbar(container, pdfDoc) {
|
||||
const toolbar = document.createElement("div");
|
||||
toolbar.className = "pdf-toolbar";
|
||||
|
||||
let currentPage = 1;
|
||||
let currentScale = parseFloat(container.dataset.pdfScale ?? "1");
|
||||
const numPages = pdfDoc.numPages;
|
||||
|
||||
const updateUI = () => {
|
||||
pageLabel.textContent = `${currentPage} / ${numPages}`;
|
||||
scaleLabel.textContent = `${Math.round(currentScale * 100)}%`;
|
||||
};
|
||||
|
||||
const prevBtn = mkBtn("‹", "Page précédente", () => {
|
||||
if (currentPage <= 1) return;
|
||||
currentPage--;
|
||||
scrollToCanvas(container, currentPage);
|
||||
updateUI();
|
||||
});
|
||||
|
||||
const nextBtn = mkBtn("›", "Page suivante", () => {
|
||||
if (currentPage >= numPages) return;
|
||||
currentPage++;
|
||||
scrollToCanvas(container, currentPage);
|
||||
updateUI();
|
||||
});
|
||||
|
||||
const pageLabel = mkLabel("");
|
||||
|
||||
const zoomOutBtn = mkBtn("−", "Zoom arrière", () => {
|
||||
currentScale = Math.max(0.5, currentScale - 0.2);
|
||||
container.dataset.pdfScale = String(currentScale);
|
||||
reRender(container, pdfDoc);
|
||||
updateUI();
|
||||
});
|
||||
|
||||
const zoomInBtn = mkBtn("+", "Zoom avant", () => {
|
||||
currentScale = Math.min(3.0, currentScale + 0.2);
|
||||
container.dataset.pdfScale = String(currentScale);
|
||||
reRender(container, pdfDoc);
|
||||
updateUI();
|
||||
});
|
||||
|
||||
const scaleLabel = mkLabel("");
|
||||
scaleLabel.classList.add("pdf-toolbar-scale");
|
||||
|
||||
const closeBtn = mkBtn("✕", "Fermer le document", () =>
|
||||
collapse(container),
|
||||
);
|
||||
closeBtn.classList.add("pdf-toolbar-close");
|
||||
|
||||
toolbar.append(
|
||||
closeBtn,
|
||||
prevBtn,
|
||||
pageLabel,
|
||||
nextBtn,
|
||||
" ",
|
||||
zoomOutBtn,
|
||||
scaleLabel,
|
||||
zoomInBtn,
|
||||
);
|
||||
container.insertBefore(toolbar, container.firstChild);
|
||||
|
||||
updateUI();
|
||||
|
||||
const canvases = container.querySelectorAll(".pdf-canvas");
|
||||
|
||||
const onScroll = () => {
|
||||
const mid = container.scrollTop + container.clientHeight * 0.4;
|
||||
let best = 1;
|
||||
let bestDist = Infinity;
|
||||
canvases.forEach((c) => {
|
||||
const pn = parseInt(c.dataset.page, 10) || 1;
|
||||
const dist = Math.abs(c.offsetTop - mid);
|
||||
if (dist < bestDist) {
|
||||
bestDist = dist;
|
||||
best = pn;
|
||||
}
|
||||
});
|
||||
if (best !== currentPage) {
|
||||
currentPage = best;
|
||||
updateUI();
|
||||
}
|
||||
};
|
||||
|
||||
container.addEventListener("scroll", onScroll, { passive: true });
|
||||
container._onScroll = onScroll;
|
||||
|
||||
container.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Escape") collapse(container);
|
||||
});
|
||||
}
|
||||
|
||||
function mkBtn(text, ariaLabel, handler) {
|
||||
const btn = document.createElement("button");
|
||||
btn.className = "pdf-toolbar-btn";
|
||||
btn.setAttribute("aria-label", ariaLabel);
|
||||
btn.textContent = text;
|
||||
btn.addEventListener("click", handler);
|
||||
return btn;
|
||||
}
|
||||
|
||||
function mkLabel(text) {
|
||||
const span = document.createElement("span");
|
||||
span.className = "pdf-toolbar-label";
|
||||
span.textContent = text;
|
||||
return span;
|
||||
}
|
||||
|
||||
function scrollToCanvas(container, pageNum) {
|
||||
const canvas = container.querySelector(
|
||||
`.pdf-canvas[data-page="${pageNum}"]`,
|
||||
);
|
||||
if (canvas) {
|
||||
canvas.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
}
|
||||
}
|
||||
|
||||
function reRender(container, pdfDoc) {
|
||||
if (container._onScroll) {
|
||||
container.removeEventListener("scroll", container._onScroll);
|
||||
}
|
||||
container.querySelectorAll(".pdf-canvas").forEach((c) => {
|
||||
c.remove();
|
||||
});
|
||||
const oldToolbar = container.querySelector(".pdf-toolbar");
|
||||
if (oldToolbar) oldToolbar.remove();
|
||||
buildToolbar(container, pdfDoc);
|
||||
renderAllPages(container, pdfDoc);
|
||||
}
|
||||
|
||||
// ── Bootstrap ──────────────────────────────────────────────────
|
||||
|
||||
bootstrap();
|
||||
})();
|
||||
@@ -18,3 +18,4 @@ import "./repertoire-scroll-restore.js";
|
||||
import "./repertoire-student-popover.js";
|
||||
import "./access-request.js";
|
||||
import "./acces-password.js";
|
||||
import "./pdf-viewer.js";
|
||||
|
||||
+29
File diff suppressed because one or more lines are too long
+29
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user