import { ViewModeMedia, ViewModeProps } from "../interface"; import { useEffect, useLayoutEffect, useRef, useState } from "preact/hooks"; import Reveal from "reveal.js"; import slideBaseStylesheet from "reveal.js/dist/reveal.css?raw"; import slideCustomStylesheet from "./slidejs.css?raw"; import { buildPresentationModel, PresentationModel, PresentationSlideBaseModel } from "./model"; import ShadowDom from "../../react/ShadowDom"; import ActionButton from "../../react/ActionButton"; import "./index.css"; import { RefObject } from "preact"; import { openInCurrentNoteContext } from "../../../components/note_context"; import { useNoteLabelWithDefault, useTriliumEvent } from "../../react/hooks"; import { t } from "../../../services/i18n"; import { DEFAULT_THEME, loadPresentationTheme } from "./themes"; import FNote from "../../../entities/fnote"; export default function PresentationView({ note, noteIds, media, onReady }: ViewModeProps<{}>) { const [ presentation, setPresentation ] = useState(); const containerRef = useRef(null); const [ api, setApi ] = useState(); const stylesheets = usePresentationStylesheets(note, media); function refresh() { buildPresentationModel(note).then(setPresentation); } useTriliumEvent("entitiesReloaded", ({ loadResults }) => { if (loadResults.getNoteIds().find(noteId => noteIds.includes(noteId)) || loadResults.getAttributeRows().find(attr => attr.noteId && attr.name?.startsWith("slide:") && noteIds.includes(attr.noteId))) { refresh(); } }); useLayoutEffect(refresh, [ note, noteIds ]); useEffect(() => { // We need to wait for Reveal.js to initialize (by setting api) and for the presentation to become available. if (api && presentation) { // Timeout is necessary because it otherwise can cause flakiness by rendering only the first slide. setTimeout(onReady, 200); } }, [ api, presentation ]); if (!presentation || !stylesheets || !note.hasChildren()) return; const content = ( <> {stylesheets.map(stylesheet => )} ); if (media === "screen") { return ( <> {content} ) } else if (media === "print") { // Printing needs a query parameter that is read by Reveal.js. const url = new URL(window.location.href); url.searchParams.set("print-pdf", ""); window.history.replaceState({}, '', url); // Shadow DOM doesn't work well with Reveal.js's PDF printing mechanism. return content; } } function usePresentationStylesheets(note: FNote, media: ViewModeMedia) { const [ themeName ] = useNoteLabelWithDefault(note, "presentation:theme", DEFAULT_THEME); const [ stylesheets, setStylesheets ] = useState(); useLayoutEffect(() => { loadPresentationTheme(themeName).then((themeStylesheet) => { let stylesheets = [ slideBaseStylesheet, themeStylesheet, slideCustomStylesheet ]; if (media === "screen") { // We are rendering in the shadow DOM, so the global variables are not set correctly. stylesheets = stylesheets.map(stylesheet => stylesheet.replace(/:root/g, ":host")); } setStylesheets(stylesheets); }); }, [ themeName ]); return stylesheets; } function ButtonOverlay({ containerRef, api }: { containerRef: RefObject, api: Reveal.Api | undefined }) { const [ isOverviewActive, setIsOverviewActive ] = useState(false); useEffect(() => { if (!api) return; setIsOverviewActive(api.isOverview()); const onEnabled = () => setIsOverviewActive(true); const onDisabled = () => setIsOverviewActive(false); api.on("overviewshown", onEnabled); api.on("overviewhidden", onDisabled); return () => { api.off("overviewshown", onEnabled); api.off("overviewhidden", onDisabled); }; }, [ api ]); return (
{ const currentSlide = api?.getCurrentSlide(); const noteId = getNoteIdFromSlide(currentSlide); if (noteId) { openInCurrentNoteContext(e, noteId); } }} /> api?.toggleOverview()} /> containerRef.current?.requestFullscreen()} />
) } function Presentation({ presentation, setApi } : { presentation: PresentationModel, setApi: (api: Reveal.Api | undefined) => void }) { const containerRef = useRef(null); const [revealApi, setRevealApi] = useState(); useEffect(() => { if (!containerRef.current) return; const api = new Reveal(containerRef.current, { transition: "slide", embedded: true, pdfMaxPagesPerSlide: 1, keyboardCondition(event) { // Full-screen requests sometimes fail, we rely on the UI button instead. if (event.key === "f") { return false; } return true; }, }); api.initialize().then(() => { setRevealApi(api); setApi(api); if (containerRef.current) { rewireLinks(containerRef.current, api); } }); return () => { api.destroy(); setRevealApi(undefined); setApi(undefined); } }, []); useEffect(() => { revealApi?.sync(); }, [ presentation, revealApi ]); return (
{presentation.slides?.map(slide => { if (!slide.verticalSlides) { return } else { return (
{slide.verticalSlides.map(slide => )}
); } })}
) } function Slide({ slide }: { slide: PresentationSlideBaseModel }) { return (
); } function getNoteIdFromSlide(slide: HTMLElement | undefined) { if (!slide) return; return slide.dataset.noteId; } function rewireLinks(container: HTMLElement, api: Reveal.Api) { const links = container.querySelectorAll("a.reference-link"); for (const link of links) { link.addEventListener("click", () => { /** * Reveal.js has built-in navigation by either index or ID. However, the ID-based navigation doesn't work because it tries to look * outside the shadom DOM (via document.getElementById). */ const url = new URL(link.href); if (!url.hash.startsWith("#/slide-")) return; const targetId = url.hash.substring(8); const slide = container.querySelector(`#slide-${targetId}`); if (!slide) return; const { h, v, f } = api.getIndices(slide); api.slide(h, v, f); }); } }