diff --git a/docs/_css/extra.css b/docs/_css/extra.css index ee1881c751..dc6fa9e828 100644 --- a/docs/_css/extra.css +++ b/docs/_css/extra.css @@ -1,4 +1,159 @@ .md-nav__source{display: none;} .md-nav--primary > .md-nav__list > .md-nav__item > .md-nav__link { font-weight: bold; +} + +/* PDF Viewer Styles */ +.pdf-viewer-container { + border: 1px solid #ccc; + border-radius: 6px; + overflow: hidden; + font-family: sans-serif; + margin-bottom: 20px; + background-color: #f5f5f5; +} + +.pdf-viewer-header { + background-color: #333; + color: white; + padding: 10px 15px; + display: flex; + justify-content: space-between; + align-items: center; +} + +.pdf-viewer-header .document-label { + font-weight: bold; + font-size: 0.9em; +} + +.pdf-viewer-header .download-btn { + text-decoration: none; + background-color: #e74c3c; + color: white !important; + padding: 6px 12px; + border-radius: 4px; + font-size: 0.85em; + transition: background 0.2s; +} + +.pdf-viewer-header .download-btn:hover { + background-color: #c0392b; +} + +.pdf-viewer-content { + position: relative; + width: 100%; + min-height: 200px; + background-color: #525659; +} + +.pdf-viewer-content canvas { + display: block; + width: 100%; + height: auto; +} + +.pdf-viewer-loading { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: white; +} + +.pdf-viewer-controls { + background-color: #ddd; + padding: 10px; + display: flex; + justify-content: center; + align-items: center; + gap: 15px; +} + +.pdf-viewer-controls button { + cursor: pointer; + padding: 5px 15px; + border: 1px solid #999; + background: #eee; + border-radius: 3px; +} + +.pdf-viewer-controls button:hover { + background: #fff; +} + +.pdf-viewer-controls .page-info { + font-size: 0.9em; +} + +/* HTML Viewer Styles */ +.html-viewer-container { + border: 1px solid #ccc; + border-radius: 6px; + overflow: hidden; + font-family: sans-serif; + margin-bottom: 20px; + background-color: #f5f5f5; +} + +.html-viewer-header { + background-color: #333; + color: white; + padding: 10px 15px; + display: flex; + justify-content: space-between; + align-items: center; +} + +.html-viewer-header .document-label { + font-weight: bold; + font-size: 0.9em; +} + +.html-viewer-header .open-btn { + text-decoration: none; + background-color: #3498db; + color: white !important; + padding: 6px 12px; + border-radius: 4px; + font-size: 0.85em; + transition: background 0.2s; +} + +.html-viewer-header .open-btn:hover { + background-color: #2980b9; +} + +.html-viewer-content { + position: relative; + width: 100%; + aspect-ratio: 16 / 9; +} + +.html-viewer-content iframe { + width: 100%; + height: 100%; + border: none; +} + +.html-viewer-controls { + background-color: #ddd; + padding: 10px; + display: flex; + justify-content: center; + align-items: center; + gap: 15px; +} + +.html-viewer-controls button { + cursor: pointer; + padding: 5px 15px; + border: 1px solid #999; + background: #eee; + border-radius: 3px; +} + +.html-viewer-controls button:hover { + background: #fff; } \ No newline at end of file diff --git a/docs/_javascripts/html-viewer.js b/docs/_javascripts/html-viewer.js new file mode 100644 index 0000000000..1f6564278a --- /dev/null +++ b/docs/_javascripts/html-viewer.js @@ -0,0 +1,66 @@ +/** + * Navigation controller for Remark.js presentations in iframes. + * + * Looks for elements with class 'html-viewer-container'. + */ + +document$.subscribe(({ body }) => { + const containers = body.querySelectorAll('.html-viewer-container'); + + containers.forEach(container => { + const iframe = container.querySelector('iframe'); + const prevBtn = container.querySelector('.prev-btn'); + const nextBtn = container.querySelector('.next-btn'); + const pageNumDisplay = container.querySelector('.page-num'); + const pageCountDisplay = container.querySelector('.page-count'); + + if (!iframe || !prevBtn || !nextBtn) return; + + function updatePageInfo() { + try { + if (iframe.contentWindow && iframe.contentWindow.slideshow) { + const slideshow = iframe.contentWindow.slideshow; + const currentIndex = slideshow.getCurrentSlideIndex() + 1; + const totalSlides = slideshow.getSlides().length; + + if (pageNumDisplay) pageNumDisplay.textContent = currentIndex; + if (pageCountDisplay) pageCountDisplay.textContent = totalSlides; + } + } catch (e) { + // Silently fail if cross-origin or slideshow not yet loaded + } + } + + prevBtn.addEventListener('click', () => { + try { + if (iframe.contentWindow && iframe.contentWindow.slideshow) { + iframe.contentWindow.slideshow.gotoPreviousSlide(); + updatePageInfo(); + } else { + iframe.contentWindow.dispatchEvent(new KeyboardEvent('keydown', { 'keyCode': 37 })); + } + } catch (e) { + console.error("Could not navigate slideshow:", e); + } + }); + + nextBtn.addEventListener('click', () => { + try { + if (iframe.contentWindow && iframe.contentWindow.slideshow) { + iframe.contentWindow.slideshow.gotoNextSlide(); + updatePageInfo(); + } else { + iframe.contentWindow.dispatchEvent(new KeyboardEvent('keydown', { 'keyCode': 39 })); + } + } catch (e) { + console.error("Could not navigate slideshow:", e); + } + }); + + // Update info when iframe loads + iframe.addEventListener('load', updatePageInfo); + + // Periodically update in case the user navigates using the keyboard inside the iframe + setInterval(updatePageInfo, 500); + }); +}); diff --git a/docs/_javascripts/pdf-viewer.js b/docs/_javascripts/pdf-viewer.js new file mode 100644 index 0000000000..671c449396 --- /dev/null +++ b/docs/_javascripts/pdf-viewer.js @@ -0,0 +1,108 @@ +/** + * Reusable PDF Viewer for MkDocs Material + * + * Uses PDF.js to render PDF files in a canvas. + * Looks for elements with class 'pdf-viewer-container' and a 'data-url' attribute. + */ + +document$.subscribe(({ body }) => { + const containers = body.querySelectorAll('.pdf-viewer-container[data-url]'); + + if (containers.length === 0) return; + + // Load PDF.js worker if not already loaded + if (window['pdfjs-dist/build/pdf'] && !window['pdfjs-dist/build/pdf'].GlobalWorkerOptions.workerSrc) { + window['pdfjs-dist/build/pdf'].GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.16.105/pdf.worker.min.js'; + } + + containers.forEach(container => { + initPDFViewer(container); + }); +}); + +function initPDFViewer(container) { + const url = container.getAttribute('data-url'); + const canvas = container.querySelector('canvas'); + const pageNumDisplay = container.querySelector('.page-num'); + const pageCountDisplay = container.querySelector('.page-count'); + const prevBtn = container.querySelector('.prev-btn'); + const nextBtn = container.querySelector('.next-btn'); + const loadingMsg = container.querySelector('.pdf-viewer-loading'); + + if (!canvas || !url) return; + + let pdfDoc = null; + let pageNum = 1; + let pageRendering = false; + let pageNumPending = null; + const scale = 2.0; + const ctx = canvas.getContext('2d'); + + function renderPage(num) { + pageRendering = true; + + pdfDoc.getPage(num).then(page => { + if (loadingMsg) loadingMsg.style.display = 'none'; + + const viewport = page.getViewport({ scale: scale }); + canvas.height = viewport.height; + canvas.width = viewport.width; + + const renderContext = { + canvasContext: ctx, + viewport: viewport + }; + const renderTask = page.render(renderContext); + + renderTask.promise.then(() => { + pageRendering = false; + if (pageNumPending !== null) { + renderPage(pageNumPending); + pageNumPending = null; + } + }); + }); + + if (pageNumDisplay) pageNumDisplay.textContent = num; + } + + function queueRenderPage(num) { + if (pageRendering) { + pageNumPending = num; + } else { + renderPage(num); + } + } + + if (prevBtn) { + prevBtn.addEventListener('click', () => { + if (pageNum <= 1) return; + pageNum--; + queueRenderPage(pageNum); + }); + } + + if (nextBtn) { + nextBtn.addEventListener('click', () => { + if (pageNum >= pdfDoc.numPages) return; + pageNum++; + queueRenderPage(pageNum); + }); + } + + // Load the document + const pdfjsLib = window['pdfjs-dist/build/pdf']; + if (!pdfjsLib) { + console.error('PDF.js library not found'); + return; + } + + pdfjsLib.getDocument(url).promise.then(pdfDoc_ => { + pdfDoc = pdfDoc_; + if (pageCountDisplay) pageCountDisplay.textContent = pdfDoc.numPages; + renderPage(pageNum); + }).catch(err => { + console.error('Error loading PDF:', err); + if (loadingMsg) loadingMsg.textContent = "Error loading PDF."; + }); +} diff --git a/docs/events.md b/docs/events.md index 224ee90a59..81812c1e47 100644 --- a/docs/events.md +++ b/docs/events.md @@ -12,7 +12,7 @@ | Date | Time | Event | Location | Link | Calendar | | ---- | ---- | ----- | -------- | ---- | -------- | -| 03. Mar 2026 | 11:00-11:45 | [Elisabet Capón:
An Introduction to Renku](events/2026-03-03_capon.md) | [OHSA/E13](https://pocket.psi.ch/psimap) | N/A | +