mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-08-10 07:11:18 +02:00
Fix lint: use IterableCallbackReturn — wrap forEach body in braces
This commit is contained in:
@@ -27,10 +27,10 @@
|
|||||||
gap: var(--space-3xs);
|
gap: var(--space-3xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Two-column layout: left 35vw (info), right fills remaining (files) */
|
/* Two-column layout: left 40vw (info), right fills remaining (files) */
|
||||||
.tfe-content-row {
|
.tfe-content-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 35vw 1fr;
|
grid-template-columns: 40vw 1fr;
|
||||||
gap: var(--space-xl);
|
gap: var(--space-xl);
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
@@ -66,6 +66,10 @@
|
|||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tfe-file-item:has(.pdf-embed-wrapper) {
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
.tfe-file-item img {
|
.tfe-file-item img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
@@ -293,6 +297,190 @@
|
|||||||
outline-color: #fc8181;
|
outline-color: #fc8181;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
PDF.js IN-PAGE VIEWER (pdf-viewer.js)
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
/*── Base wrapper ───────────────────────────── */
|
||||||
|
|
||||||
|
.pdf-embed-wrapper {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
outline: none;
|
||||||
|
transition: height 0.35s ease, border-color 0.35s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Collapsed state: thumbnail tile ────────── */
|
||||||
|
|
||||||
|
.pdf-collapsed {
|
||||||
|
cursor: pointer;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
min-height: 200px;
|
||||||
|
max-height: 350px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-thumbnail {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Overlay veil + enter button ────────────── */
|
||||||
|
|
||||||
|
.pdf-overlay {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgb(0 0 0 / 0.15);
|
||||||
|
backdrop-filter: blur(4px) saturate(0.6);
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-overlay:hover {
|
||||||
|
background: rgb(0 0 0 / 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Document type pill — top-left of the overlay */
|
||||||
|
|
||||||
|
.pdf-overlay-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: var(--space-xs);
|
||||||
|
left: var(--space-xs);
|
||||||
|
z-index: 2;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.15em 0.6em;
|
||||||
|
font-size: var(--step--2);
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: color-mix(in srgb, var(--accent-primary) 90%, #000);
|
||||||
|
background: color-mix(in srgb, var(--accent-primary) 12%, transparent);
|
||||||
|
border: 1px solid color-mix(in srgb, var(--accent-primary) 25%, transparent);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-enter-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2xs);
|
||||||
|
padding: var(--space-xs) var(--space-m);
|
||||||
|
background: var(--accent-primary);
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
font-size: var(--step-0);
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 2px 8px rgb(0 0 0 / 0.2);
|
||||||
|
transition: transform 0.15s, box-shadow 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-enter-btn:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgb(0 0 0 / 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Expanded state: wrapper becomes scroll panel ── */
|
||||||
|
|
||||||
|
.pdf-expanded {
|
||||||
|
/* Fill viewport minus the column's vertical padding */
|
||||||
|
max-height: calc(100vh - var(--space-xl) * 2);
|
||||||
|
height: clamp(400px, 80vh, calc(100vh - var(--space-xl) * 2));
|
||||||
|
overflow-y: auto;
|
||||||
|
border: 2px solid var(--accent-primary);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Toolbar ────────────────────────────────── */
|
||||||
|
|
||||||
|
.pdf-toolbar {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 2;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-3xs);
|
||||||
|
padding: var(--space-2xs) var(--space-xs);
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: var(--step--1);
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-toolbar-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 2em;
|
||||||
|
height: 2em;
|
||||||
|
padding: 0;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--text-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: inherit;
|
||||||
|
line-height: 1;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-toolbar-btn:hover {
|
||||||
|
background: color-mix(in srgb, var(--accent-primary) 15%, var(--surface));
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-toolbar-close {
|
||||||
|
margin-right: var(--space-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-toolbar-label {
|
||||||
|
min-width: 4.5em;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-toolbar-scale {
|
||||||
|
min-width: 3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Scroll container ────────────────────────── */
|
||||||
|
|
||||||
|
.pdf-canvas {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
box-shadow: 0 1px 4px rgb(0 0 0 / 0.12);
|
||||||
|
background: #fff;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-canvas:first-of-type {
|
||||||
|
margin-top: var(--space-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-canvas:last-of-type {
|
||||||
|
margin-bottom: var(--space-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-canvas + .pdf-canvas {
|
||||||
|
margin-top: var(--space-2xs);
|
||||||
|
}
|
||||||
|
|
||||||
/* Responsive */
|
/* Responsive */
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
.tfe-main {
|
.tfe-main {
|
||||||
|
|||||||
@@ -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 "./repertoire-student-popover.js";
|
||||||
import "./access-request.js";
|
import "./access-request.js";
|
||||||
import "./acces-password.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
@@ -16,6 +16,7 @@ if (
|
|||||||
[
|
[
|
||||||
"css",
|
"css",
|
||||||
"js",
|
"js",
|
||||||
|
"mjs",
|
||||||
"png",
|
"png",
|
||||||
"jpg",
|
"jpg",
|
||||||
"jpeg",
|
"jpeg",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<main class="tfe-main" id="main-content">
|
<main class="tfe-main" id="main-content">
|
||||||
<article class="tfe-layout">
|
<article class="tfe-layout">
|
||||||
|
|
||||||
<!-- Two-column layout: left 35% (info), right 65% (files) -->
|
<!-- Two-column layout: left 40% (info), right 60% (files) -->
|
||||||
<div class="tfe-content-row">
|
<div class="tfe-content-row">
|
||||||
<div class="tfe-left-column">
|
<div class="tfe-left-column">
|
||||||
|
|
||||||
@@ -46,22 +46,22 @@
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
$_hasPages = isset($data["duration_pages"]) && $data["duration_pages"] !== null;
|
$_hasPages = isset($data["duration_pages"]) && $data["duration_pages"] !== null && $data["duration_pages"] !== '';
|
||||||
$_hasMins = isset($data["duration_minutes"]) && $data["duration_minutes"] !== null;
|
$_hasMins = isset($data["duration_minutes"]) && $data["duration_minutes"] !== null && $data["duration_minutes"] !== '';
|
||||||
if ($_hasPages || $_hasMins):
|
|
||||||
$_parts = [];
|
$_parts = [];
|
||||||
if ($_hasPages) $_parts[] = $data['duration_pages'] . ' pages';
|
if ($_hasPages) $_parts[] = $data['duration_pages'] . ' pages';
|
||||||
if ($_hasMins) {
|
if ($_hasMins) {
|
||||||
$_h = (int)floor($data['duration_minutes'] / 60);
|
$_h = (int)floor((int)$data['duration_minutes'] / 60);
|
||||||
$_m = $data['duration_minutes'] % 60;
|
$_m = (int)$data['duration_minutes'] % 60;
|
||||||
$_parts[] = $_h . 'h' . str_pad((string)$_m, 2, '0', STR_PAD_LEFT);
|
$_parts[] = $_h . 'h' . str_pad((string)$_m, 2, '0', STR_PAD_LEFT);
|
||||||
}
|
}
|
||||||
|
$_duree = $_parts ? htmlspecialchars(implode(', ', $_parts)) : '–';
|
||||||
|
unset($_hasPages, $_hasMins, $_parts, $_h, $_m);
|
||||||
?>
|
?>
|
||||||
<p class="tfe-meta-item">
|
<p class="tfe-meta-item">
|
||||||
<span class="tfe-meta-label">Durée :</span>
|
<span class="tfe-meta-label">Durée :</span>
|
||||||
<?= htmlspecialchars(implode(', ', $_parts)) ?>
|
<?= $_duree ?>
|
||||||
</p>
|
</p>
|
||||||
<?php unset($_hasPages, $_hasMins, $_parts, $_h, $_m); endif; ?>
|
|
||||||
|
|
||||||
<?php if (!empty($data["languages"])): ?>
|
<?php if (!empty($data["languages"])): ?>
|
||||||
<p class="tfe-meta-item">
|
<p class="tfe-meta-item">
|
||||||
@@ -376,10 +376,19 @@
|
|||||||
?>
|
?>
|
||||||
<div class="tfe-file-item">
|
<div class="tfe-file-item">
|
||||||
<?php if ($isPdf): ?>
|
<?php if ($isPdf): ?>
|
||||||
<iframe src="<?= $mediaUrl ?>#toolbar=0&navpanes=0&scrollbar=0"
|
<?php
|
||||||
class="tfe-file-iframe"
|
$_pdfLabel = match ($fileType) {
|
||||||
title="<?= $fileName ?>">
|
'main' => 'TFE',
|
||||||
</iframe>
|
'annex' => 'Annexes',
|
||||||
|
'note_intention' => "Note d'intention",
|
||||||
|
default => '',
|
||||||
|
};
|
||||||
|
?>
|
||||||
|
<div class="pdf-embed-wrapper"
|
||||||
|
data-pdf-url="<?= $mediaUrl ?>"
|
||||||
|
data-pdf-scale="1"
|
||||||
|
data-pdf-label="<?= htmlspecialchars($_pdfLabel) ?>"
|
||||||
|
tabindex="-1"></div>
|
||||||
<?php elseif ($isWebsite): ?>
|
<?php elseif ($isWebsite): ?>
|
||||||
<iframe src="<?= $mediaUrl ?>"
|
<iframe src="<?= $mediaUrl ?>"
|
||||||
class="tfe-file-iframe"
|
class="tfe-file-iframe"
|
||||||
@@ -458,3 +467,10 @@
|
|||||||
|
|
||||||
</article>
|
</article>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<?php if (!$isInterdit && !$shouldHideFiles && !empty($data["files"])): ?>
|
||||||
|
<?php $_hasPdf = false; foreach ($data["files"] as $_f) { $_ext = strtolower(pathinfo($_f["file_path"] ?? '', PATHINFO_EXTENSION)); $_type = $_f['file_type'] ?? ''; if ($_ext === 'pdf' || $_type === 'main') { $_hasPdf = true; break; } } ?>
|
||||||
|
<?php if ($_hasPdf): ?>
|
||||||
|
<script type="module" src="/assets/js/vendor/pdf.min.mjs" async></script>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ server {
|
|||||||
text/css
|
text/css
|
||||||
text/javascript
|
text/javascript
|
||||||
application/javascript
|
application/javascript
|
||||||
|
text/javascript
|
||||||
application/json
|
application/json
|
||||||
application/xml
|
application/xml
|
||||||
text/xml
|
text/xml
|
||||||
@@ -243,6 +244,16 @@ server {
|
|||||||
access_log off;
|
access_log off;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# JavaScript modules (.mjs) — explicit MIME type for ES modules
|
||||||
|
# Some nginx installs lack the mjs entry in mime.types
|
||||||
|
location ~* \.mjs$ {
|
||||||
|
types { }
|
||||||
|
default_type text/javascript;
|
||||||
|
expires 30d;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
access_log off;
|
||||||
|
}
|
||||||
|
|
||||||
# PDF files (thesis documents)
|
# PDF files (thesis documents)
|
||||||
location ~* \.pdf$ {
|
location ~* \.pdf$ {
|
||||||
expires 7d;
|
expires 7d;
|
||||||
|
|||||||
Reference in New Issue
Block a user