import SlideContent from "./controllers/slidecontent.js"; import SlideNumber from "./controllers/slidenumber.js"; import JumpToSlide from "./controllers/jumptoslide.js"; import Backgrounds from "./controllers/backgrounds.js"; import AutoAnimate from "./controllers/autoanimate.js"; import Fragments from "./controllers/fragments.js"; import Overview from "./controllers/overview.js"; import Keyboard from "./controllers/keyboard.js"; import Location from "./controllers/location.js"; import Controls from "./controllers/controls.js"; import Progress from "./controllers/progress.js"; import Pointer from "./controllers/pointer.js"; import Plugins from "./controllers/plugins.js"; import Print from "./controllers/print.js"; import Touch from "./controllers/touch.js"; import Focus from "./controllers/focus.js"; import Notes from "./controllers/notes.js"; import Playback from "./components/playback.js"; import defaultConfig from "./config.js"; import * as Util from "./utils/util.js"; import * as Device from "./utils/device.js"; import { SLIDES_SELECTOR, HORIZONTAL_SLIDES_SELECTOR, VERTICAL_SLIDES_SELECTOR, POST_MESSAGE_METHOD_BLACKLIST, } from "./utils/constants.js"; // The reveal.js version export const VERSION = "4.5.0"; /** * reveal.js * https://revealjs.com * MIT licensed * * Copyright (C) 2011-2022 Hakim El Hattab, https://hakim.se */ export default function (revealElement, options) { // Support initialization with no args, one arg // [options] or two args [revealElement, options] if (arguments.length < 2) { options = arguments[0]; revealElement = document.querySelector(".reveal"); } const Reveal = {}; // Configuration defaults, can be overridden at initialization time let config = {}, // Flags if reveal.js is loaded (has dispatched the 'ready' event) ready = false, // The horizontal and vertical index of the currently active slide indexh, indexv, // The previous and current slide HTML elements previousSlide, currentSlide, // Remember which directions that the user has navigated towards navigationHistory = { hasNavigatedHorizontally: false, hasNavigatedVertically: false, }, // Slides may have a data-state attribute which we pick up and apply // as a class to the body. This list contains the combined state of // all current slides. state = [], // The current scale of the presentation (see width/height config) scale = 1, // CSS transform that is currently applied to the slides container, // split into two groups slidesTransform = { layout: "", overview: "" }, // Cached references to DOM elements dom = {}, // Flags if the interaction event listeners are bound eventsAreBound = false, // The current slide transition state; idle or running transition = "idle", // The current auto-slide duration autoSlide = 0, // Auto slide properties autoSlidePlayer, autoSlideTimeout = 0, autoSlideStartTime = -1, autoSlidePaused = false, // Controllers for different aspects of our presentation. They're // all given direct references to this Reveal instance since there // may be multiple presentations running in parallel. slideContent = new SlideContent(Reveal), slideNumber = new SlideNumber(Reveal), jumpToSlide = new JumpToSlide(Reveal), autoAnimate = new AutoAnimate(Reveal), backgrounds = new Backgrounds(Reveal), fragments = new Fragments(Reveal), overview = new Overview(Reveal), keyboard = new Keyboard(Reveal), location = new Location(Reveal), controls = new Controls(Reveal), progress = new Progress(Reveal), pointer = new Pointer(Reveal), plugins = new Plugins(Reveal), print = new Print(Reveal), focus = new Focus(Reveal), touch = new Touch(Reveal), notes = new Notes(Reveal); /** * Starts up the presentation. */ function initialize(initOptions) { if (!revealElement) throw 'Unable to find presentation root (
).'; // Cache references to key DOM elements dom.wrapper = revealElement; dom.slides = revealElement.querySelector(".slides"); if (!dom.slides) throw 'Unable to find slides container (
).'; // Compose our config object in order of increasing precedence: // 1. Default reveal.js options // 2. Options provided via Reveal.configure() prior to // initialization // 3. Options passed to the Reveal constructor // 4. Options passed to Reveal.initialize // 5. Query params config = { ...defaultConfig, ...config, ...options, ...initOptions, ...Util.getQueryHash(), }; setViewport(); // Force a layout when the whole page, incl fonts, has loaded window.addEventListener("load", layout, false); // Register plugins and load dependencies, then move on to #start() plugins.load(config.plugins, config.dependencies).then(start); return new Promise((resolve) => Reveal.on("ready", resolve)); } /** * Encase the presentation in a reveal.js viewport. The * extent of the viewport differs based on configuration. */ function setViewport() { // Embedded decks use the reveal element as their viewport if (config.embedded === true) { dom.viewport = Util.closest(revealElement, ".reveal-viewport") || revealElement; } // Full-page decks use the body as their viewport else { dom.viewport = document.body; document.documentElement.classList.add("reveal-full-page"); } dom.viewport.classList.add("reveal-viewport"); } /** * Starts up reveal.js by binding input events and navigating * to the current URL deeplink if there is one. */ function start() { ready = true; // Remove slides hidden with data-visibility removeHiddenSlides(); // Make sure we've got all the DOM elements we need setupDOM(); // Listen to messages posted to this window setupPostMessage(); // Prevent the slides from being scrolled out of view setupScrollPrevention(); // Adds bindings for fullscreen mode setupFullscreen(); // Resets all vertical slides so that only the first is visible resetVerticalSlides(); // Updates the presentation to match the current configuration values configure(); // Read the initial hash location.readURL(); // Create slide backgrounds backgrounds.update(true); // Notify listeners that the presentation is ready but use a 1ms // timeout to ensure it's not fired synchronously after #initialize() setTimeout(() => { // Enable transitions now that we're loaded dom.slides.classList.remove("no-transition"); dom.wrapper.classList.add("ready"); dispatchEvent({ type: "ready", data: { indexh, indexv, currentSlide, }, }); }, 1); // Special setup and config is required when printing to PDF if (print.isPrintingPDF()) { removeEventListeners(); // The document needs to have loaded for the PDF layout // measurements to be accurate if (document.readyState === "complete") { print.setupPDF(); } else { window.addEventListener("load", () => { print.setupPDF(); }); } } } /** * Removes all slides with data-visibility="hidden". This * is done right before the rest of the presentation is * initialized. * * If you want to show all hidden slides, initialize * reveal.js with showHiddenSlides set to true. */ function removeHiddenSlides() { if (!config.showHiddenSlides) { Util.queryAll(dom.wrapper, 'section[data-visibility="hidden"]').forEach( (slide) => { slide.parentNode.removeChild(slide); } ); } } /** * Finds and stores references to DOM elements which are * required by the presentation. If a required element is * not found, it is created. */ function setupDOM() { // Prevent transitions while we're loading dom.slides.classList.add("no-transition"); if (Device.isMobile) { dom.wrapper.classList.add("no-hover"); } else { dom.wrapper.classList.remove("no-hover"); } backgrounds.render(); slideNumber.render(); jumpToSlide.render(); controls.render(); progress.render(); notes.render(); // Overlay graphic which is displayed during the paused mode dom.pauseOverlay = Util.createSingletonNode( dom.wrapper, "div", "pause-overlay", config.controls ? '' : null ); dom.statusElement = createStatusElement(); dom.wrapper.setAttribute("role", "application"); } /** * Creates a hidden div with role aria-live to announce the * current slide content. Hide the div off-screen to make it * available only to Assistive Technologies. * * @return {HTMLElement} */ function createStatusElement() { let statusElement = dom.wrapper.querySelector(".aria-status"); if (!statusElement) { statusElement = document.createElement("div"); statusElement.style.position = "absolute"; statusElement.style.height = "1px"; statusElement.style.width = "1px"; statusElement.style.overflow = "hidden"; statusElement.style.clip = "rect( 1px, 1px, 1px, 1px )"; statusElement.classList.add("aria-status"); statusElement.setAttribute("aria-live", "polite"); statusElement.setAttribute("aria-atomic", "true"); dom.wrapper.appendChild(statusElement); } return statusElement; } /** * Announces the given text to screen readers. */ function announceStatus(value) { dom.statusElement.textContent = value; } /** * Converts the given HTML element into a string of text * that can be announced to a screen reader. Hidden * elements are excluded. */ function getStatusText(node) { let text = ""; // Text node if (node.nodeType === 3) { text += node.textContent; } // Element node else if (node.nodeType === 1) { let isAriaHidden = node.getAttribute("aria-hidden"); let isDisplayHidden = window.getComputedStyle(node)["display"] === "none"; if (isAriaHidden !== "true" && !isDisplayHidden) { Array.from(node.childNodes).forEach((child) => { text += getStatusText(child); }); } } text = text.trim(); return text === "" ? "" : text + " "; } /** * This is an unfortunate necessity. Some actions – such as * an input field being focused in an iframe or using the * keyboard to expand text selection beyond the bounds of * a slide – can trigger our content to be pushed out of view. * This scrolling can not be prevented by hiding overflow in * CSS (we already do) so we have to resort to repeatedly * checking if the slides have been offset :( */ function setupScrollPrevention() { setInterval(() => { if (dom.wrapper.scrollTop !== 0 || dom.wrapper.scrollLeft !== 0) { dom.wrapper.scrollTop = 0; dom.wrapper.scrollLeft = 0; } }, 1000); } /** * After entering fullscreen we need to force a layout to * get our presentations to scale correctly. This behavior * is inconsistent across browsers but a force layout seems * to normalize it. */ function setupFullscreen() { document.addEventListener("fullscreenchange", onFullscreenChange); document.addEventListener("webkitfullscreenchange", onFullscreenChange); } /** * Registers a listener to postMessage events, this makes it * possible to call all reveal.js API methods from another * window. For example: * * revealWindow.postMessage( JSON.stringify({ * method: 'slide', * args: [ 2 ] * }), '*' ); */ function setupPostMessage() { if (config.postMessage) { window.addEventListener("message", onPostMessage, false); } } /** * Applies the configuration settings from the config * object. May be called multiple times. * * @param {object} options */ function configure(options) { const oldConfig = { ...config }; // New config options may be passed when this method // is invoked through the API after initialization if (typeof options === "object") Util.extend(config, options); // Abort if reveal.js hasn't finished loading, config // changes will be applied automatically once ready if (Reveal.isReady() === false) return; const numberOfSlides = dom.wrapper.querySelectorAll(SLIDES_SELECTOR).length; // The transition is added as a class on the .reveal element dom.wrapper.classList.remove(oldConfig.transition); dom.wrapper.classList.add(config.transition); dom.wrapper.setAttribute("data-transition-speed", config.transitionSpeed); dom.wrapper.setAttribute( "data-background-transition", config.backgroundTransition ); // Expose our configured slide dimensions as custom props dom.viewport.style.setProperty("--slide-width", config.width + "px"); dom.viewport.style.setProperty("--slide-height", config.height + "px"); if (config.shuffle) { shuffle(); } Util.toggleClass(dom.wrapper, "embedded", config.embedded); Util.toggleClass(dom.wrapper, "rtl", config.rtl); Util.toggleClass(dom.wrapper, "center", config.center); // Exit the paused mode if it was configured off if (config.pause === false) { resume(); } // Iframe link previews if (config.previewLinks) { enablePreviewLinks(); disablePreviewLinks("[data-preview-link=false]"); } else { disablePreviewLinks(); enablePreviewLinks("[data-preview-link]:not([data-preview-link=false])"); } // Reset all changes made by auto-animations autoAnimate.reset(); // Remove existing auto-slide controls if (autoSlidePlayer) { autoSlidePlayer.destroy(); autoSlidePlayer = null; } // Generate auto-slide controls if needed if (numberOfSlides > 1 && config.autoSlide && config.autoSlideStoppable) { autoSlidePlayer = new Playback(dom.wrapper, () => { return Math.min( Math.max((Date.now() - autoSlideStartTime) / autoSlide, 0), 1 ); }); autoSlidePlayer.on("click", onAutoSlidePlayerClick); autoSlidePaused = false; } // Add the navigation mode to the DOM so we can adjust styling if (config.navigationMode !== "default") { dom.wrapper.setAttribute("data-navigation-mode", config.navigationMode); } else { dom.wrapper.removeAttribute("data-navigation-mode"); } notes.configure(config, oldConfig); focus.configure(config, oldConfig); pointer.configure(config, oldConfig); controls.configure(config, oldConfig); progress.configure(config, oldConfig); keyboard.configure(config, oldConfig); fragments.configure(config, oldConfig); slideNumber.configure(config, oldConfig); sync(); } /** * Binds all event listeners. */ function addEventListeners() { eventsAreBound = true; window.addEventListener("resize", onWindowResize, false); if (config.touch) touch.bind(); if (config.keyboard) keyboard.bind(); if (config.progress) progress.bind(); if (config.respondToHashChanges) location.bind(); controls.bind(); focus.bind(); dom.slides.addEventListener("click", onSlidesClicked, false); dom.slides.addEventListener("transitionend", onTransitionEnd, false); dom.pauseOverlay.addEventListener("click", resume, false); if (config.focusBodyOnPageVisibilityChange) { document.addEventListener( "visibilitychange", onPageVisibilityChange, false ); } } /** * Unbinds all event listeners. */ function removeEventListeners() { eventsAreBound = false; touch.unbind(); focus.unbind(); keyboard.unbind(); controls.unbind(); progress.unbind(); location.unbind(); window.removeEventListener("resize", onWindowResize, false); dom.slides.removeEventListener("click", onSlidesClicked, false); dom.slides.removeEventListener("transitionend", onTransitionEnd, false); dom.pauseOverlay.removeEventListener("click", resume, false); } /** * Uninitializes reveal.js by undoing changes made to the * DOM and removing all event listeners. */ function destroy() { removeEventListeners(); cancelAutoSlide(); disablePreviewLinks(); // Destroy controllers notes.destroy(); focus.destroy(); plugins.destroy(); pointer.destroy(); controls.destroy(); progress.destroy(); backgrounds.destroy(); slideNumber.destroy(); jumpToSlide.destroy(); // Remove event listeners document.removeEventListener("fullscreenchange", onFullscreenChange); document.removeEventListener("webkitfullscreenchange", onFullscreenChange); document.removeEventListener( "visibilitychange", onPageVisibilityChange, false ); window.removeEventListener("message", onPostMessage, false); window.removeEventListener("load", layout, false); // Undo DOM changes if (dom.pauseOverlay) dom.pauseOverlay.remove(); if (dom.statusElement) dom.statusElement.remove(); document.documentElement.classList.remove("reveal-full-page"); dom.wrapper.classList.remove( "ready", "center", "has-horizontal-slides", "has-vertical-slides" ); dom.wrapper.removeAttribute("data-transition-speed"); dom.wrapper.removeAttribute("data-background-transition"); dom.viewport.classList.remove("reveal-viewport"); dom.viewport.style.removeProperty("--slide-width"); dom.viewport.style.removeProperty("--slide-height"); dom.slides.style.removeProperty("width"); dom.slides.style.removeProperty("height"); dom.slides.style.removeProperty("zoom"); dom.slides.style.removeProperty("left"); dom.slides.style.removeProperty("top"); dom.slides.style.removeProperty("bottom"); dom.slides.style.removeProperty("right"); dom.slides.style.removeProperty("transform"); Array.from(dom.wrapper.querySelectorAll(SLIDES_SELECTOR)).forEach( (slide) => { slide.style.removeProperty("display"); slide.style.removeProperty("top"); slide.removeAttribute("hidden"); slide.removeAttribute("aria-hidden"); } ); } /** * Adds a listener to one of our custom reveal.js events, * like slidechanged. */ function on(type, listener, useCapture) { revealElement.addEventListener(type, listener, useCapture); } /** * Unsubscribes from a reveal.js event. */ function off(type, listener, useCapture) { revealElement.removeEventListener(type, listener, useCapture); } /** * Applies CSS transforms to the slides container. The container * is transformed from two separate sources: layout and the overview * mode. * * @param {object} transforms */ function transformSlides(transforms) { // Pick up new transforms from arguments if (typeof transforms.layout === "string") slidesTransform.layout = transforms.layout; if (typeof transforms.overview === "string") slidesTransform.overview = transforms.overview; // Apply the transforms to the slides container if (slidesTransform.layout) { Util.transformElement( dom.slides, slidesTransform.layout + " " + slidesTransform.overview ); } else { Util.transformElement(dom.slides, slidesTransform.overview); } } /** * Dispatches an event of the specified type from the * reveal DOM element. */ function dispatchEvent({ target = dom.wrapper, type, data, bubbles = true }) { let event = document.createEvent("HTMLEvents", 1, 2); event.initEvent(type, bubbles, true); Util.extend(event, data); target.dispatchEvent(event); if (target === dom.wrapper) { // If we're in an iframe, post each reveal.js event to the // parent window. Used by the notes plugin dispatchPostMessage(type); } return event; } /** * Dispatched a postMessage of the given type from our window. */ function dispatchPostMessage(type, data) { if (config.postMessageEvents && window.parent !== window.self) { let message = { namespace: "reveal", eventName: type, state: getState(), }; Util.extend(message, data); window.parent.postMessage(JSON.stringify(message), "*"); } } /** * Bind preview frame links. * * @param {string} [selector=a] - selector for anchors */ function enablePreviewLinks(selector = "a") { Array.from(dom.wrapper.querySelectorAll(selector)).forEach((element) => { if (/^(http|www)/gi.test(element.getAttribute("href"))) { element.addEventListener("click", onPreviewLinkClicked, false); } }); } /** * Unbind preview frame links. */ function disablePreviewLinks(selector = "a") { Array.from(dom.wrapper.querySelectorAll(selector)).forEach((element) => { if (/^(http|www)/gi.test(element.getAttribute("href"))) { element.removeEventListener("click", onPreviewLinkClicked, false); } }); } /** * Opens a preview window for the target URL. * * @param {string} url - url for preview iframe src */ function showPreview(url) { closeOverlay(); dom.overlay = document.createElement("div"); dom.overlay.classList.add("overlay"); dom.overlay.classList.add("overlay-preview"); dom.wrapper.appendChild(dom.overlay); dom.overlay.innerHTML = `
Unable to load iframe. This is likely due to the site's policy (x-frame-options).
`; dom.overlay.querySelector("iframe").addEventListener( "load", (event) => { dom.overlay.classList.add("loaded"); }, false ); dom.overlay.querySelector(".close").addEventListener( "click", (event) => { closeOverlay(); event.preventDefault(); }, false ); dom.overlay.querySelector(".external").addEventListener( "click", (event) => { closeOverlay(); }, false ); } /** * Open or close help overlay window. * * @param {Boolean} [override] Flag which overrides the * toggle logic and forcibly sets the desired state. True means * help is open, false means it's closed. */ function toggleHelp(override) { if (typeof override === "boolean") { override ? showHelp() : closeOverlay(); } else { if (dom.overlay) { closeOverlay(); } else { showHelp(); } } } /** * Opens an overlay window with help material. */ function showHelp() { if (config.help) { closeOverlay(); dom.overlay = document.createElement("div"); dom.overlay.classList.add("overlay"); dom.overlay.classList.add("overlay-help"); dom.wrapper.appendChild(dom.overlay); let html = '

Keyboard Shortcuts


'; let shortcuts = keyboard.getShortcuts(), bindings = keyboard.getBindings(); html += ""; for (let key in shortcuts) { html += ``; } // Add custom key bindings that have associated descriptions for (let binding in bindings) { if (bindings[binding].key && bindings[binding].description) { html += ``; } } html += "
KEYACTION
${key}${shortcuts[key]}
${bindings[binding].key}${bindings[binding].description}
"; dom.overlay.innerHTML = `
${html}
`; dom.overlay.querySelector(".close").addEventListener( "click", (event) => { closeOverlay(); event.preventDefault(); }, false ); } } /** * Closes any currently open overlay. */ function closeOverlay() { if (dom.overlay) { dom.overlay.parentNode.removeChild(dom.overlay); dom.overlay = null; return true; } return false; } /** * Applies JavaScript-controlled layout rules to the * presentation. */ function layout() { if (dom.wrapper && !print.isPrintingPDF()) { if (!config.disableLayout) { // On some mobile devices '100vh' is taller than the visible // viewport which leads to part of the presentation being // cut off. To work around this we define our own '--vh' custom // property where 100x adds up to the correct height. // // https://css-tricks.com/the-trick-to-viewport-units-on-mobile/ if (Device.isMobile && !config.embedded) { document.documentElement.style.setProperty( "--vh", window.innerHeight * 0.01 + "px" ); } const size = getComputedSlideSize(); const oldScale = scale; // Layout the contents of the slides layoutSlideContents(config.width, config.height); dom.slides.style.width = size.width + "px"; dom.slides.style.height = size.height + "px"; // Determine scale of content to fit within available space scale = Math.min( size.presentationWidth / size.width, size.presentationHeight / size.height ); // Respect max/min scale settings scale = Math.max(scale, config.minScale); scale = Math.min(scale, config.maxScale); // Don't apply any scaling styles if scale is 1 if (scale === 1) { dom.slides.style.zoom = ""; dom.slides.style.left = ""; dom.slides.style.top = ""; dom.slides.style.bottom = ""; dom.slides.style.right = ""; transformSlides({ layout: "" }); } else { dom.slides.style.zoom = ""; dom.slides.style.left = "50%"; dom.slides.style.top = "50%"; dom.slides.style.bottom = "auto"; dom.slides.style.right = "auto"; transformSlides({ layout: "translate(-50%, -50%) scale(" + scale + ")", }); } // Select all slides, vertical and horizontal const slides = Array.from( dom.wrapper.querySelectorAll(SLIDES_SELECTOR) ); for (let i = 0, len = slides.length; i < len; i++) { const slide = slides[i]; // Don't bother updating invisible slides if (slide.style.display === "none") { continue; } if (config.center || slide.classList.contains("center")) { // Vertical stacks are not centred since their section // children will be if (slide.classList.contains("stack")) { slide.style.top = 0; } else { slide.style.top = Math.max((size.height - slide.scrollHeight) / 2, 0) + "px"; } } else { slide.style.top = ""; } } if (oldScale !== scale) { dispatchEvent({ type: "resize", data: { oldScale, scale, size, }, }); } } dom.viewport.style.setProperty("--slide-scale", scale); progress.update(); backgrounds.updateParallax(); if (overview.isActive()) { overview.update(); } } } /** * Applies layout logic to the contents of all slides in * the presentation. * * @param {string|number} width * @param {string|number} height */ function layoutSlideContents(width, height) { // Handle sizing of elements with the 'r-stretch' class Util.queryAll( dom.slides, "section > .stretch, section > .r-stretch" ).forEach((element) => { // Determine how much vertical space we can use let remainingHeight = Util.getRemainingHeight(element, height); // Consider the aspect ratio of media elements if (/(img|video)/gi.test(element.nodeName)) { const nw = element.naturalWidth || element.videoWidth, nh = element.naturalHeight || element.videoHeight; const es = Math.min(width / nw, remainingHeight / nh); element.style.width = nw * es + "px"; element.style.height = nh * es + "px"; } else { element.style.width = width + "px"; element.style.height = remainingHeight + "px"; } }); } /** * Calculates the computed pixel size of our slides. These * values are based on the width and height configuration * options. * * @param {number} [presentationWidth=dom.wrapper.offsetWidth] * @param {number} [presentationHeight=dom.wrapper.offsetHeight] */ function getComputedSlideSize(presentationWidth, presentationHeight) { let width = config.width; let height = config.height; if (config.disableLayout) { width = dom.slides.offsetWidth; height = dom.slides.offsetHeight; } const size = { // Slide size width: width, height: height, // Presentation size presentationWidth: presentationWidth || dom.wrapper.offsetWidth, presentationHeight: presentationHeight || dom.wrapper.offsetHeight, }; // Reduce available space by margin size.presentationWidth -= size.presentationWidth * config.margin; size.presentationHeight -= size.presentationHeight * config.margin; // Slide width may be a percentage of available width if (typeof size.width === "string" && /%$/.test(size.width)) { size.width = (parseInt(size.width, 10) / 100) * size.presentationWidth; } // Slide height may be a percentage of available height if (typeof size.height === "string" && /%$/.test(size.height)) { size.height = (parseInt(size.height, 10) / 100) * size.presentationHeight; } return size; } /** * Stores the vertical index of a stack so that the same * vertical slide can be selected when navigating to and * from the stack. * * @param {HTMLElement} stack The vertical stack element * @param {string|number} [v=0] Index to memorize */ function setPreviousVerticalIndex(stack, v) { if (typeof stack === "object" && typeof stack.setAttribute === "function") { stack.setAttribute("data-previous-indexv", v || 0); } } /** * Retrieves the vertical index which was stored using * #setPreviousVerticalIndex() or 0 if no previous index * exists. * * @param {HTMLElement} stack The vertical stack element */ function getPreviousVerticalIndex(stack) { if ( typeof stack === "object" && typeof stack.setAttribute === "function" && stack.classList.contains("stack") ) { // Prefer manually defined start-indexv const attributeName = stack.hasAttribute("data-start-indexv") ? "data-start-indexv" : "data-previous-indexv"; return parseInt(stack.getAttribute(attributeName) || 0, 10); } return 0; } /** * Checks if the current or specified slide is vertical * (nested within another slide). * * @param {HTMLElement} [slide=currentSlide] The slide to check * orientation of * @return {Boolean} */ function isVerticalSlide(slide = currentSlide) { return ( slide && slide.parentNode && !!slide.parentNode.nodeName.match(/section/i) ); } /** * Returns true if we're on the last slide in the current * vertical stack. */ function isLastVerticalSlide() { if (currentSlide && isVerticalSlide(currentSlide)) { // Does this slide have a next sibling? if (currentSlide.nextElementSibling) return false; return true; } return false; } /** * Returns true if we're currently on the first slide in * the presentation. */ function isFirstSlide() { return indexh === 0 && indexv === 0; } /** * Returns true if we're currently on the last slide in * the presenation. If the last slide is a stack, we only * consider this the last slide if it's at the end of the * stack. */ function isLastSlide() { if (currentSlide) { // Does this slide have a next sibling? if (currentSlide.nextElementSibling) return false; // If it's vertical, does its parent have a next sibling? if ( isVerticalSlide(currentSlide) && currentSlide.parentNode.nextElementSibling ) return false; return true; } return false; } /** * Enters the paused mode which fades everything on screen to * black. */ function pause() { if (config.pause) { const wasPaused = dom.wrapper.classList.contains("paused"); cancelAutoSlide(); dom.wrapper.classList.add("paused"); if (wasPaused === false) { dispatchEvent({ type: "paused" }); } } } /** * Exits from the paused mode. */ function resume() { const wasPaused = dom.wrapper.classList.contains("paused"); dom.wrapper.classList.remove("paused"); cueAutoSlide(); if (wasPaused) { dispatchEvent({ type: "resumed" }); } } /** * Toggles the paused mode on and off. */ function togglePause(override) { if (typeof override === "boolean") { override ? pause() : resume(); } else { isPaused() ? resume() : pause(); } } /** * Checks if we are currently in the paused mode. * * @return {Boolean} */ function isPaused() { return dom.wrapper.classList.contains("paused"); } /** * Toggles visibility of the jump-to-slide UI. */ function toggleJumpToSlide(override) { if (typeof override === "boolean") { override ? jumpToSlide.show() : jumpToSlide.hide(); } else { jumpToSlide.isVisible() ? jumpToSlide.hide() : jumpToSlide.show(); } } /** * Toggles the auto slide mode on and off. * * @param {Boolean} [override] Flag which sets the desired state. * True means autoplay starts, false means it stops. */ function toggleAutoSlide(override) { if (typeof override === "boolean") { override ? resumeAutoSlide() : pauseAutoSlide(); } else { autoSlidePaused ? resumeAutoSlide() : pauseAutoSlide(); } } /** * Checks if the auto slide mode is currently on. * * @return {Boolean} */ function isAutoSliding() { return !!(autoSlide && !autoSlidePaused); } /** * Steps from the current point in the presentation to the * slide which matches the specified horizontal and vertical * indices. * * @param {number} [h=indexh] Horizontal index of the target slide * @param {number} [v=indexv] Vertical index of the target slide * @param {number} [f] Index of a fragment within the * target slide to activate * @param {number} [origin] Origin for use in multimaster environments */ function slide(h, v, f, origin) { // Dispatch an event before the slide const slidechange = dispatchEvent({ type: "beforeslidechange", data: { indexh: h === undefined ? indexh : h, indexv: v === undefined ? indexv : v, origin, }, }); // Abort if this slide change was prevented by an event listener if (slidechange.defaultPrevented) return; // Remember where we were at before previousSlide = currentSlide; // Query all horizontal slides in the deck const horizontalSlides = dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ); // Abort if there are no slides if (horizontalSlides.length === 0) return; // If no vertical index is specified and the upcoming slide is a // stack, resume at its previous vertical index if (v === undefined && !overview.isActive()) { v = getPreviousVerticalIndex(horizontalSlides[h]); } // If we were on a vertical stack, remember what vertical index // it was on so we can resume at the same position when returning if ( previousSlide && previousSlide.parentNode && previousSlide.parentNode.classList.contains("stack") ) { setPreviousVerticalIndex(previousSlide.parentNode, indexv); } // Remember the state before this slide const stateBefore = state.concat(); // Reset the state array state.length = 0; let indexhBefore = indexh || 0, indexvBefore = indexv || 0; // Activate and transition to the new slide indexh = updateSlides( HORIZONTAL_SLIDES_SELECTOR, h === undefined ? indexh : h ); indexv = updateSlides( VERTICAL_SLIDES_SELECTOR, v === undefined ? indexv : v ); // Dispatch an event if the slide changed let slideChanged = indexh !== indexhBefore || indexv !== indexvBefore; // Ensure that the previous slide is never the same as the current if (!slideChanged) previousSlide = null; // Find the current horizontal slide and any possible vertical slides // within it let currentHorizontalSlide = horizontalSlides[indexh], currentVerticalSlides = currentHorizontalSlide.querySelectorAll("section"); // Store references to the previous and current slides currentSlide = currentVerticalSlides[indexv] || currentHorizontalSlide; let autoAnimateTransition = false; // Detect if we're moving between two auto-animated slides if (slideChanged && previousSlide && currentSlide && !overview.isActive()) { // If this is an auto-animated transition, we disable the // regular slide transition // // Note 20-03-2020: // This needs to happen before we update slide visibility, // otherwise transitions will still run in Safari. if ( previousSlide.hasAttribute("data-auto-animate") && currentSlide.hasAttribute("data-auto-animate") && previousSlide.getAttribute("data-auto-animate-id") === currentSlide.getAttribute("data-auto-animate-id") && !( indexh > indexhBefore || indexv > indexvBefore ? currentSlide : previousSlide ).hasAttribute("data-auto-animate-restart") ) { autoAnimateTransition = true; dom.slides.classList.add("disable-slide-transitions"); } transition = "running"; } // Update the visibility of slides now that the indices have changed updateSlidesVisibility(); layout(); // Update the overview if it's currently active if (overview.isActive()) { overview.update(); } // Show fragment, if specified if (typeof f !== "undefined") { fragments.goto(f); } // Solves an edge case where the previous slide maintains the // 'present' class when navigating between adjacent vertical // stacks if (previousSlide && previousSlide !== currentSlide) { previousSlide.classList.remove("present"); previousSlide.setAttribute("aria-hidden", "true"); // Reset all slides upon navigate to home if (isFirstSlide()) { // Launch async task setTimeout(() => { getVerticalStacks().forEach((slide) => { setPreviousVerticalIndex(slide, 0); }); }, 0); } } // Apply the new state stateLoop: for (let i = 0, len = state.length; i < len; i++) { // Check if this state existed on the previous slide. If it // did, we will avoid adding it repeatedly for (let j = 0; j < stateBefore.length; j++) { if (stateBefore[j] === state[i]) { stateBefore.splice(j, 1); continue stateLoop; } } dom.viewport.classList.add(state[i]); // Dispatch custom event matching the state's name dispatchEvent({ type: state[i] }); } // Clean up the remains of the previous state while (stateBefore.length) { dom.viewport.classList.remove(stateBefore.pop()); } if (slideChanged) { dispatchEvent({ type: "slidechanged", data: { indexh, indexv, previousSlide, currentSlide, origin, }, }); } // Handle embedded content if (slideChanged || !previousSlide) { slideContent.stopEmbeddedContent(previousSlide); slideContent.startEmbeddedContent(currentSlide); } // Announce the current slide contents to screen readers // Use animation frame to prevent getComputedStyle in getStatusText // from triggering layout mid-frame requestAnimationFrame(() => { announceStatus(getStatusText(currentSlide)); }); progress.update(); controls.update(); notes.update(); backgrounds.update(); backgrounds.updateParallax(); slideNumber.update(); fragments.update(); // Update the URL hash location.writeURL(); cueAutoSlide(); // Auto-animation if (autoAnimateTransition) { setTimeout(() => { dom.slides.classList.remove("disable-slide-transitions"); }, 0); if (config.autoAnimate) { // Run the auto-animation between our slides autoAnimate.run(previousSlide, currentSlide); } } } /** * Syncs the presentation with the current DOM. Useful * when new slides or control elements are added or when * the configuration has changed. */ function sync() { // Subscribe to input removeEventListeners(); addEventListeners(); // Force a layout to make sure the current config is accounted for layout(); // Reflect the current autoSlide value autoSlide = config.autoSlide; // Start auto-sliding if it's enabled cueAutoSlide(); // Re-create all slide backgrounds backgrounds.create(); // Write the current hash to the URL location.writeURL(); if (config.sortFragmentsOnSync === true) { fragments.sortAll(); } controls.update(); progress.update(); updateSlidesVisibility(); notes.update(); notes.updateVisibility(); backgrounds.update(true); slideNumber.update(); slideContent.formatEmbeddedContent(); // Start or stop embedded content depending on global config if (config.autoPlayMedia === false) { slideContent.stopEmbeddedContent(currentSlide, { unloadIframes: false }); } else { slideContent.startEmbeddedContent(currentSlide); } if (overview.isActive()) { overview.layout(); } } /** * Updates reveal.js to keep in sync with new slide attributes. For * example, if you add a new `data-background-image` you can call * this to have reveal.js render the new background image. * * Similar to #sync() but more efficient when you only need to * refresh a specific slide. * * @param {HTMLElement} slide */ function syncSlide(slide = currentSlide) { backgrounds.sync(slide); fragments.sync(slide); slideContent.load(slide); backgrounds.update(); notes.update(); } /** * Resets all vertical slides so that only the first * is visible. */ function resetVerticalSlides() { getHorizontalSlides().forEach((horizontalSlide) => { Util.queryAll(horizontalSlide, "section").forEach((verticalSlide, y) => { if (y > 0) { verticalSlide.classList.remove("present"); verticalSlide.classList.remove("past"); verticalSlide.classList.add("future"); verticalSlide.setAttribute("aria-hidden", "true"); } }); }); } /** * Randomly shuffles all slides in the deck. */ function shuffle(slides = getHorizontalSlides()) { slides.forEach((slide, i) => { // Insert the slide next to a randomly picked sibling slide // slide. This may cause the slide to insert before itself, // but that's not an issue. let beforeSlide = slides[Math.floor(Math.random() * slides.length)]; if (beforeSlide.parentNode === slide.parentNode) { slide.parentNode.insertBefore(slide, beforeSlide); } // Randomize the order of vertical slides (if there are any) let verticalSlides = slide.querySelectorAll("section"); if (verticalSlides.length) { shuffle(verticalSlides); } }); } /** * Updates one dimension of slides by showing the slide * with the specified index. * * @param {string} selector A CSS selector that will fetch * the group of slides we are working with * @param {number} index The index of the slide that should be * shown * * @return {number} The index of the slide that is now shown, * might differ from the passed in index if it was out of * bounds. */ function updateSlides(selector, index) { // Select all slides and convert the NodeList result to // an array let slides = Util.queryAll(dom.wrapper, selector), slidesLength = slides.length; let printMode = print.isPrintingPDF(); let loopedForwards = false; let loopedBackwards = false; if (slidesLength) { // Should the index loop? if (config.loop) { if (index >= slidesLength) loopedForwards = true; index %= slidesLength; if (index < 0) { index = slidesLength + index; loopedBackwards = true; } } // Enforce max and minimum index bounds index = Math.max(Math.min(index, slidesLength - 1), 0); for (let i = 0; i < slidesLength; i++) { let element = slides[i]; let reverse = config.rtl && !isVerticalSlide(element); // Avoid .remove() with multiple args for IE11 support element.classList.remove("past"); element.classList.remove("present"); element.classList.remove("future"); // http://www.w3.org/html/wg/drafts/html/master/editing.html#the-hidden-attribute element.setAttribute("hidden", ""); element.setAttribute("aria-hidden", "true"); // If this element contains vertical slides if (element.querySelector("section")) { element.classList.add("stack"); } // If we're printing static slides, all slides are "present" if (printMode) { element.classList.add("present"); continue; } if (i < index) { // Any element previous to index is given the 'past' class element.classList.add(reverse ? "future" : "past"); if (config.fragments) { // Show all fragments in prior slides showFragmentsIn(element); } } else if (i > index) { // Any element subsequent to index is given the 'future' class element.classList.add(reverse ? "past" : "future"); if (config.fragments) { // Hide all fragments in future slides hideFragmentsIn(element); } } // Update the visibility of fragments when a presentation loops // in either direction else if (i === index && config.fragments) { if (loopedForwards) { hideFragmentsIn(element); } else if (loopedBackwards) { showFragmentsIn(element); } } } let slide = slides[index]; let wasPresent = slide.classList.contains("present"); // Mark the current slide as present slide.classList.add("present"); slide.removeAttribute("hidden"); slide.removeAttribute("aria-hidden"); if (!wasPresent) { // Dispatch an event indicating the slide is now visible dispatchEvent({ target: slide, type: "visible", bubbles: false, }); } // If this slide has a state associated with it, add it // onto the current state of the deck let slideState = slide.getAttribute("data-state"); if (slideState) { state = state.concat(slideState.split(" ")); } } else { // Since there are no slides we can't be anywhere beyond the // zeroth index index = 0; } return index; } /** * Shows all fragment elements within the given contaienr. */ function showFragmentsIn(container) { Util.queryAll(container, ".fragment").forEach((fragment) => { fragment.classList.add("visible"); fragment.classList.remove("current-fragment"); }); } /** * Hides all fragment elements within the given contaienr. */ function hideFragmentsIn(container) { Util.queryAll(container, ".fragment.visible").forEach((fragment) => { fragment.classList.remove("visible", "current-fragment"); }); } /** * Optimization method; hide all slides that are far away * from the present slide. */ function updateSlidesVisibility() { // Select all slides and convert the NodeList result to // an array let horizontalSlides = getHorizontalSlides(), horizontalSlidesLength = horizontalSlides.length, distanceX, distanceY; if (horizontalSlidesLength && typeof indexh !== "undefined") { // The number of steps away from the present slide that will // be visible let viewDistance = overview.isActive() ? 10 : config.viewDistance; // Shorten the view distance on devices that typically have // less resources if (Device.isMobile) { viewDistance = overview.isActive() ? 6 : config.mobileViewDistance; } // All slides need to be visible when exporting to PDF if (print.isPrintingPDF()) { viewDistance = Number.MAX_VALUE; } for (let x = 0; x < horizontalSlidesLength; x++) { let horizontalSlide = horizontalSlides[x]; let verticalSlides = Util.queryAll(horizontalSlide, "section"), verticalSlidesLength = verticalSlides.length; // Determine how far away this slide is from the present distanceX = Math.abs((indexh || 0) - x) || 0; // If the presentation is looped, distance should measure // 1 between the first and last slides if (config.loop) { distanceX = Math.abs( ((indexh || 0) - x) % (horizontalSlidesLength - viewDistance) ) || 0; } // Show the horizontal slide if it's within the view distance if (distanceX < viewDistance) { slideContent.load(horizontalSlide); } else { slideContent.unload(horizontalSlide); } if (verticalSlidesLength) { let oy = getPreviousVerticalIndex(horizontalSlide); for (let y = 0; y < verticalSlidesLength; y++) { let verticalSlide = verticalSlides[y]; distanceY = x === (indexh || 0) ? Math.abs((indexv || 0) - y) : Math.abs(y - oy); if (distanceX + distanceY < viewDistance) { slideContent.load(verticalSlide); } else { slideContent.unload(verticalSlide); } } } } // Flag if there are ANY vertical slides, anywhere in the deck if (hasVerticalSlides()) { dom.wrapper.classList.add("has-vertical-slides"); } else { dom.wrapper.classList.remove("has-vertical-slides"); } // Flag if there are ANY horizontal slides, anywhere in the deck if (hasHorizontalSlides()) { dom.wrapper.classList.add("has-horizontal-slides"); } else { dom.wrapper.classList.remove("has-horizontal-slides"); } } } /** * Determine what available routes there are for navigation. * * @return {{left: boolean, right: boolean, up: boolean, down: boolean}} */ function availableRoutes({ includeFragments = false } = {}) { let horizontalSlides = dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ), verticalSlides = dom.wrapper.querySelectorAll(VERTICAL_SLIDES_SELECTOR); let routes = { left: indexh > 0, right: indexh < horizontalSlides.length - 1, up: indexv > 0, down: indexv < verticalSlides.length - 1, }; // Looped presentations can always be navigated as long as // there are slides available if (config.loop) { if (horizontalSlides.length > 1) { routes.left = true; routes.right = true; } if (verticalSlides.length > 1) { routes.up = true; routes.down = true; } } if (horizontalSlides.length > 1 && config.navigationMode === "linear") { routes.right = routes.right || routes.down; routes.left = routes.left || routes.up; } // If includeFragments is set, a route will be considered // available if either a slid OR fragment is available in // the given direction if (includeFragments === true) { let fragmentRoutes = fragments.availableRoutes(); routes.left = routes.left || fragmentRoutes.prev; routes.up = routes.up || fragmentRoutes.prev; routes.down = routes.down || fragmentRoutes.next; routes.right = routes.right || fragmentRoutes.next; } // Reverse horizontal controls for rtl if (config.rtl) { let left = routes.left; routes.left = routes.right; routes.right = left; } return routes; } /** * Returns the number of past slides. This can be used as a global * flattened index for slides. * * @param {HTMLElement} [slide=currentSlide] The slide we're counting before * * @return {number} Past slide count */ function getSlidePastCount(slide = currentSlide) { let horizontalSlides = getHorizontalSlides(); // The number of past slides let pastCount = 0; // Step through all slides and count the past ones mainLoop: for (let i = 0; i < horizontalSlides.length; i++) { let horizontalSlide = horizontalSlides[i]; let verticalSlides = horizontalSlide.querySelectorAll("section"); for (let j = 0; j < verticalSlides.length; j++) { // Stop as soon as we arrive at the present if (verticalSlides[j] === slide) { break mainLoop; } // Don't count slides with the "uncounted" class if (verticalSlides[j].dataset.visibility !== "uncounted") { pastCount++; } } // Stop as soon as we arrive at the present if (horizontalSlide === slide) { break; } // Don't count the wrapping section for vertical slides and // slides marked as uncounted if ( horizontalSlide.classList.contains("stack") === false && horizontalSlide.dataset.visibility !== "uncounted" ) { pastCount++; } } return pastCount; } /** * Returns a value ranging from 0-1 that represents * how far into the presentation we have navigated. * * @return {number} */ function getProgress() { // The number of past and total slides let totalCount = getTotalSlides(); let pastCount = getSlidePastCount(); if (currentSlide) { let allFragments = currentSlide.querySelectorAll(".fragment"); // If there are fragments in the current slide those should be // accounted for in the progress. if (allFragments.length > 0) { let visibleFragments = currentSlide.querySelectorAll(".fragment.visible"); // This value represents how big a portion of the slide progress // that is made up by its fragments (0-1) let fragmentWeight = 0.9; // Add fragment progress to the past slide count pastCount += (visibleFragments.length / allFragments.length) * fragmentWeight; } } return Math.min(pastCount / (totalCount - 1), 1); } /** * Retrieves the h/v location and fragment of the current, * or specified, slide. * * @param {HTMLElement} [slide] If specified, the returned * index will be for this slide rather than the currently * active one * * @return {{h: number, v: number, f: number}} */ function getIndices(slide) { // By default, return the current indices let h = indexh, v = indexv, f; // If a slide is specified, return the indices of that slide if (slide) { let isVertical = isVerticalSlide(slide); let slideh = isVertical ? slide.parentNode : slide; // Select all horizontal slides let horizontalSlides = getHorizontalSlides(); // Now that we know which the horizontal slide is, get its index h = Math.max(horizontalSlides.indexOf(slideh), 0); // Assume we're not vertical v = undefined; // If this is a vertical slide, grab the vertical index if (isVertical) { v = Math.max( Util.queryAll(slide.parentNode, "section").indexOf(slide), 0 ); } } if (!slide && currentSlide) { let hasFragments = currentSlide.querySelectorAll(".fragment").length > 0; if (hasFragments) { let currentFragment = currentSlide.querySelector(".current-fragment"); if ( currentFragment && currentFragment.hasAttribute("data-fragment-index") ) { f = parseInt(currentFragment.getAttribute("data-fragment-index"), 10); } else { f = currentSlide.querySelectorAll(".fragment.visible").length - 1; } } } return { h, v, f }; } /** * Retrieves all slides in this presentation. */ function getSlides() { return Util.queryAll( dom.wrapper, SLIDES_SELECTOR + ':not(.stack):not([data-visibility="uncounted"])' ); } /** * Returns a list of all horizontal slides in the deck. Each * vertical stack is included as one horizontal slide in the * resulting array. */ function getHorizontalSlides() { return Util.queryAll(dom.wrapper, HORIZONTAL_SLIDES_SELECTOR); } /** * Returns all vertical slides that exist within this deck. */ function getVerticalSlides() { return Util.queryAll(dom.wrapper, ".slides>section>section"); } /** * Returns all vertical stacks (each stack can contain multiple slides). */ function getVerticalStacks() { return Util.queryAll(dom.wrapper, HORIZONTAL_SLIDES_SELECTOR + ".stack"); } /** * Returns true if there are at least two horizontal slides. */ function hasHorizontalSlides() { return getHorizontalSlides().length > 1; } /** * Returns true if there are at least two vertical slides. */ function hasVerticalSlides() { return getVerticalSlides().length > 1; } /** * Returns an array of objects where each object represents the * attributes on its respective slide. */ function getSlidesAttributes() { return getSlides().map((slide) => { let attributes = {}; for (let i = 0; i < slide.attributes.length; i++) { let attribute = slide.attributes[i]; attributes[attribute.name] = attribute.value; } return attributes; }); } /** * Retrieves the total number of slides in this presentation. * * @return {number} */ function getTotalSlides() { return getSlides().length; } /** * Returns the slide element matching the specified index. * * @return {HTMLElement} */ function getSlide(x, y) { let horizontalSlide = getHorizontalSlides()[x]; let verticalSlides = horizontalSlide && horizontalSlide.querySelectorAll("section"); if (verticalSlides && verticalSlides.length && typeof y === "number") { return verticalSlides ? verticalSlides[y] : undefined; } return horizontalSlide; } /** * Returns the background element for the given slide. * All slides, even the ones with no background properties * defined, have a background element so as long as the * index is valid an element will be returned. * * @param {mixed} x Horizontal background index OR a slide * HTML element * @param {number} y Vertical background index * @return {(HTMLElement[]|*)} */ function getSlideBackground(x, y) { let slide = typeof x === "number" ? getSlide(x, y) : x; if (slide) { return slide.slideBackgroundElement; } return undefined; } /** * Retrieves the current state of the presentation as * an object. This state can then be restored at any * time. * * @return {{indexh: number, indexv: number, indexf: number, paused: boolean, overview: boolean}} */ function getState() { let indices = getIndices(); return { indexh: indices.h, indexv: indices.v, indexf: indices.f, paused: isPaused(), overview: overview.isActive(), }; } /** * Restores the presentation to the given state. * * @param {object} state As generated by getState() * @see {@link getState} generates the parameter `state` */ function setState(state) { if (typeof state === "object") { slide( Util.deserialize(state.indexh), Util.deserialize(state.indexv), Util.deserialize(state.indexf) ); let pausedFlag = Util.deserialize(state.paused), overviewFlag = Util.deserialize(state.overview); if (typeof pausedFlag === "boolean" && pausedFlag !== isPaused()) { togglePause(pausedFlag); } if ( typeof overviewFlag === "boolean" && overviewFlag !== overview.isActive() ) { overview.toggle(overviewFlag); } } } /** * Cues a new automated slide if enabled in the config. */ function cueAutoSlide() { cancelAutoSlide(); if (currentSlide && config.autoSlide !== false) { let fragment = currentSlide.querySelector(".current-fragment"); // When the slide first appears there is no "current" fragment so // we look for a data-autoslide timing on the first fragment if (!fragment) fragment = currentSlide.querySelector(".fragment"); let fragmentAutoSlide = fragment ? fragment.getAttribute("data-autoslide") : null; let parentAutoSlide = currentSlide.parentNode ? currentSlide.parentNode.getAttribute("data-autoslide") : null; let slideAutoSlide = currentSlide.getAttribute("data-autoslide"); // Pick value in the following priority order: // 1. Current fragment's data-autoslide // 2. Current slide's data-autoslide // 3. Parent slide's data-autoslide // 4. Global autoSlide setting if (fragmentAutoSlide) { autoSlide = parseInt(fragmentAutoSlide, 10); } else if (slideAutoSlide) { autoSlide = parseInt(slideAutoSlide, 10); } else if (parentAutoSlide) { autoSlide = parseInt(parentAutoSlide, 10); } else { autoSlide = config.autoSlide; // If there are media elements with data-autoplay, // automatically set the autoSlide duration to the // length of that media. Not applicable if the slide // is divided up into fragments. // playbackRate is accounted for in the duration. if (currentSlide.querySelectorAll(".fragment").length === 0) { Util.queryAll(currentSlide, "video, audio").forEach((el) => { if (el.hasAttribute("data-autoplay")) { if ( autoSlide && (el.duration * 1000) / el.playbackRate > autoSlide ) { autoSlide = (el.duration * 1000) / el.playbackRate + 1000; } } }); } } // Cue the next auto-slide if: // - There is an autoSlide value // - Auto-sliding isn't paused by the user // - The presentation isn't paused // - The overview isn't active // - The presentation isn't over if ( autoSlide && !autoSlidePaused && !isPaused() && !overview.isActive() && (!isLastSlide() || fragments.availableRoutes().next || config.loop === true) ) { autoSlideTimeout = setTimeout(() => { if (typeof config.autoSlideMethod === "function") { config.autoSlideMethod(); } else { navigateNext(); } cueAutoSlide(); }, autoSlide); autoSlideStartTime = Date.now(); } if (autoSlidePlayer) { autoSlidePlayer.setPlaying(autoSlideTimeout !== -1); } } } /** * Cancels any ongoing request to auto-slide. */ function cancelAutoSlide() { clearTimeout(autoSlideTimeout); autoSlideTimeout = -1; } function pauseAutoSlide() { if (autoSlide && !autoSlidePaused) { autoSlidePaused = true; dispatchEvent({ type: "autoslidepaused" }); clearTimeout(autoSlideTimeout); if (autoSlidePlayer) { autoSlidePlayer.setPlaying(false); } } } function resumeAutoSlide() { if (autoSlide && autoSlidePaused) { autoSlidePaused = false; dispatchEvent({ type: "autoslideresumed" }); cueAutoSlide(); } } function navigateLeft({ skipFragments = false } = {}) { navigationHistory.hasNavigatedHorizontally = true; // Reverse for RTL if (config.rtl) { if ( (overview.isActive() || skipFragments || fragments.next() === false) && availableRoutes().left ) { slide( indexh + 1, config.navigationMode === "grid" ? indexv : undefined ); } } // Normal navigation else if ( (overview.isActive() || skipFragments || fragments.prev() === false) && availableRoutes().left ) { slide(indexh - 1, config.navigationMode === "grid" ? indexv : undefined); } } function navigateRight({ skipFragments = false } = {}) { navigationHistory.hasNavigatedHorizontally = true; // Reverse for RTL if (config.rtl) { if ( (overview.isActive() || skipFragments || fragments.prev() === false) && availableRoutes().right ) { slide( indexh - 1, config.navigationMode === "grid" ? indexv : undefined ); } } // Normal navigation else if ( (overview.isActive() || skipFragments || fragments.next() === false) && availableRoutes().right ) { slide(indexh + 1, config.navigationMode === "grid" ? indexv : undefined); } } function navigateUp({ skipFragments = false } = {}) { // Prioritize hiding fragments if ( (overview.isActive() || skipFragments || fragments.prev() === false) && availableRoutes().up ) { slide(indexh, indexv - 1); } } function navigateDown({ skipFragments = false } = {}) { navigationHistory.hasNavigatedVertically = true; // Prioritize revealing fragments if ( (overview.isActive() || skipFragments || fragments.next() === false) && availableRoutes().down ) { slide(indexh, indexv + 1); } } /** * Navigates backwards, prioritized in the following order: * 1) Previous fragment * 2) Previous vertical slide * 3) Previous horizontal slide */ function navigatePrev({ skipFragments = false } = {}) { // Prioritize revealing fragments if (skipFragments || fragments.prev() === false) { if (availableRoutes().up) { navigateUp({ skipFragments }); } else { // Fetch the previous horizontal slide, if there is one let previousSlide; if (config.rtl) { previousSlide = Util.queryAll( dom.wrapper, HORIZONTAL_SLIDES_SELECTOR + ".future" ).pop(); } else { previousSlide = Util.queryAll( dom.wrapper, HORIZONTAL_SLIDES_SELECTOR + ".past" ).pop(); } // When going backwards and arriving on a stack we start // at the bottom of the stack if (previousSlide && previousSlide.classList.contains("stack")) { let v = previousSlide.querySelectorAll("section").length - 1 || undefined; let h = indexh - 1; slide(h, v); } else { navigateLeft({ skipFragments }); } } } } /** * The reverse of #navigatePrev(). */ function navigateNext({ skipFragments = false } = {}) { navigationHistory.hasNavigatedHorizontally = true; navigationHistory.hasNavigatedVertically = true; // Prioritize revealing fragments if (skipFragments || fragments.next() === false) { let routes = availableRoutes(); // When looping is enabled `routes.down` is always available // so we need a separate check for when we've reached the // end of a stack and should move horizontally if (routes.down && routes.right && config.loop && isLastVerticalSlide()) { routes.down = false; } if (routes.down) { navigateDown({ skipFragments }); } else if (config.rtl) { navigateLeft({ skipFragments }); } else { navigateRight({ skipFragments }); } } } // --------------------------------------------------------------------// // ----------------------------- EVENTS -------------------------------// // --------------------------------------------------------------------// /** * Called by all event handlers that are based on user * input. * * @param {object} [event] */ function onUserInput(event) { if (config.autoSlideStoppable) { pauseAutoSlide(); } } /** * Listener for post message events posted to this window. */ function onPostMessage(event) { let data = event.data; // Make sure we're dealing with JSON if ( typeof data === "string" && data.charAt(0) === "{" && data.charAt(data.length - 1) === "}" ) { data = JSON.parse(data); // Check if the requested method can be found if (data.method && typeof Reveal[data.method] === "function") { if (POST_MESSAGE_METHOD_BLACKLIST.test(data.method) === false) { const result = Reveal[data.method].apply(Reveal, data.args); // Dispatch a postMessage event with the returned value from // our method invocation for getter functions dispatchPostMessage("callback", { method: data.method, result: result, }); } else { console.warn( 'reveal.js: "' + data.method + '" is is blacklisted from the postMessage API' ); } } } } /** * Event listener for transition end on the current slide. * * @param {object} [event] */ function onTransitionEnd(event) { if (transition === "running" && /section/gi.test(event.target.nodeName)) { transition = "idle"; dispatchEvent({ type: "slidetransitionend", data: { indexh, indexv, previousSlide, currentSlide }, }); } } /** * A global listener for all click events inside of the * .slides container. * * @param {object} [event] */ function onSlidesClicked(event) { const anchor = Util.closest(event.target, 'a[href^="#"]'); // If a hash link is clicked, we find the target slide // and navigate to it. We previously relied on 'hashchange' // for links like these but that prevented media with // audio tracks from playing in mobile browsers since it // wasn't considered a direct interaction with the document. if (anchor) { const hash = anchor.getAttribute("href"); const indices = location.getIndicesFromHash(hash); if (indices) { Reveal.slide(indices.h, indices.v, indices.f); event.preventDefault(); } } } /** * Handler for the window level 'resize' event. * * @param {object} [event] */ function onWindowResize(event) { layout(); } /** * Handle for the window level 'visibilitychange' event. * * @param {object} [event] */ function onPageVisibilityChange(event) { // If, after clicking a link or similar and we're coming back, // focus the document.body to ensure we can use keyboard shortcuts if (document.hidden === false && document.activeElement !== document.body) { // Not all elements support .blur() - SVGs among them. if (typeof document.activeElement.blur === "function") { document.activeElement.blur(); } document.body.focus(); } } /** * Handler for the document level 'fullscreenchange' event. * * @param {object} [event] */ function onFullscreenChange(event) { let element = document.fullscreenElement || document.webkitFullscreenElement; if (element === dom.wrapper) { event.stopImmediatePropagation(); // Timeout to avoid layout shift in Safari setTimeout(() => { Reveal.layout(); Reveal.focus.focus(); // focus.focus :'( }, 1); } } /** * Handles clicks on links that are set to preview in the * iframe overlay. * * @param {object} event */ function onPreviewLinkClicked(event) { if (event.currentTarget && event.currentTarget.hasAttribute("href")) { let url = event.currentTarget.getAttribute("href"); if (url) { showPreview(url); event.preventDefault(); } } } /** * Handles click on the auto-sliding controls element. * * @param {object} [event] */ function onAutoSlidePlayerClick(event) { // Replay if (isLastSlide() && config.loop === false) { slide(0, 0); resumeAutoSlide(); } // Resume else if (autoSlidePaused) { resumeAutoSlide(); } // Pause else { pauseAutoSlide(); } } // --------------------------------------------------------------------// // ------------------------------- API --------------------------------// // --------------------------------------------------------------------// // The public reveal.js API const API = { VERSION, initialize, configure, destroy, sync, syncSlide, syncFragments: fragments.sync.bind(fragments), // Navigation methods slide, left: navigateLeft, right: navigateRight, up: navigateUp, down: navigateDown, prev: navigatePrev, next: navigateNext, // Navigation aliases navigateLeft, navigateRight, navigateUp, navigateDown, navigatePrev, navigateNext, // Fragment methods navigateFragment: fragments.goto.bind(fragments), prevFragment: fragments.prev.bind(fragments), nextFragment: fragments.next.bind(fragments), // Event binding on, off, // Legacy event binding methods left in for backwards compatibility addEventListener: on, removeEventListener: off, // Forces an update in slide layout layout, // Randomizes the order of slides shuffle, // Returns an object with the available routes as booleans (left/right/top/bottom) availableRoutes, // Returns an object with the available fragments as booleans (prev/next) availableFragments: fragments.availableRoutes.bind(fragments), // Toggles a help overlay with keyboard shortcuts toggleHelp, // Toggles the overview mode on/off toggleOverview: overview.toggle.bind(overview), // Toggles the "black screen" mode on/off togglePause, // Toggles the auto slide mode on/off toggleAutoSlide, // Toggles visibility of the jump-to-slide UI toggleJumpToSlide, // Slide navigation checks isFirstSlide, isLastSlide, isLastVerticalSlide, isVerticalSlide, // State checks isPaused, isAutoSliding, isSpeakerNotes: notes.isSpeakerNotesWindow.bind(notes), isOverview: overview.isActive.bind(overview), isFocused: focus.isFocused.bind(focus), isPrintingPDF: print.isPrintingPDF.bind(print), // Checks if reveal.js has been loaded and is ready for use isReady: () => ready, // Slide preloading loadSlide: slideContent.load.bind(slideContent), unloadSlide: slideContent.unload.bind(slideContent), // Preview management showPreview, hidePreview: closeOverlay, // Adds or removes all internal event listeners addEventListeners, removeEventListeners, dispatchEvent, // Facility for persisting and restoring the presentation state getState, setState, // Presentation progress on range of 0-1 getProgress, // Returns the indices of the current, or specified, slide getIndices, // Returns an Array of key:value maps of the attributes of each // slide in the deck getSlidesAttributes, // Returns the number of slides that we have passed getSlidePastCount, // Returns the total number of slides getTotalSlides, // Returns the slide element at the specified index getSlide, // Returns the previous slide element, may be null getPreviousSlide: () => previousSlide, // Returns the current slide element getCurrentSlide: () => currentSlide, // Returns the slide background element at the specified index getSlideBackground, // Returns the speaker notes string for a slide, or null getSlideNotes: notes.getSlideNotes.bind(notes), // Returns an Array of all slides getSlides, // Returns an array with all horizontal/vertical slides in the deck getHorizontalSlides, getVerticalSlides, // Checks if the presentation contains two or more horizontal // and vertical slides hasHorizontalSlides, hasVerticalSlides, // Checks if the deck has navigated on either axis at least once hasNavigatedHorizontally: () => navigationHistory.hasNavigatedHorizontally, hasNavigatedVertically: () => navigationHistory.hasNavigatedVertically, // Adds/removes a custom key binding addKeyBinding: keyboard.addKeyBinding.bind(keyboard), removeKeyBinding: keyboard.removeKeyBinding.bind(keyboard), // Programmatically triggers a keyboard event triggerKey: keyboard.triggerKey.bind(keyboard), // Registers a new shortcut to include in the help overlay registerKeyboardShortcut: keyboard.registerKeyboardShortcut.bind(keyboard), getComputedSlideSize, // Returns the current scale of the presentation content getScale: () => scale, // Returns the current configuration object getConfig: () => config, // Helper method, retrieves query string as a key:value map getQueryHash: Util.getQueryHash, // Returns the path to the current slide as represented in the URL getSlidePath: location.getHash.bind(location), // Returns reveal.js DOM elements getRevealElement: () => revealElement, getSlidesElement: () => dom.slides, getViewportElement: () => dom.viewport, getBackgroundsElement: () => backgrounds.element, // API for registering and retrieving plugins registerPlugin: plugins.registerPlugin.bind(plugins), hasPlugin: plugins.hasPlugin.bind(plugins), getPlugin: plugins.getPlugin.bind(plugins), getPlugins: plugins.getRegisteredPlugins.bind(plugins), }; // Our internal API which controllers have access to Util.extend(Reveal, { ...API, // Methods for announcing content to screen readers announceStatus, getStatusText, // Controllers print, focus, progress, controls, location, overview, fragments, slideContent, slideNumber, onUserInput, closeOverlay, updateSlidesVisibility, layoutSlideContents, transformSlides, cueAutoSlide, cancelAutoSlide, }); return API; }