2775 lines
78 KiB
JavaScript
2775 lines
78 KiB
JavaScript
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 (<div class="reveal">).';
|
||
|
||
// Cache references to key DOM elements
|
||
dom.wrapper = revealElement;
|
||
dom.slides = revealElement.querySelector(".slides");
|
||
|
||
if (!dom.slides)
|
||
throw 'Unable to find slides container (<div class="slides">).';
|
||
|
||
// 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
|
||
? '<button class="resume-button">Resume presentation</button>'
|
||
: 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 = `<header>
|
||
<a class="close" href="#"><span class="icon"></span></a>
|
||
<a class="external" href="${url}" target="_blank"><span class="icon"></span></a>
|
||
</header>
|
||
<div class="spinner"></div>
|
||
<div class="viewport">
|
||
<iframe src="${url}"></iframe>
|
||
<small class="viewport-inner">
|
||
<span class="x-frame-error">Unable to load iframe. This is likely due to the site's policy (x-frame-options).</span>
|
||
</small>
|
||
</div>`;
|
||
|
||
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 = '<p class="title">Keyboard Shortcuts</p><br/>';
|
||
|
||
let shortcuts = keyboard.getShortcuts(),
|
||
bindings = keyboard.getBindings();
|
||
|
||
html += "<table><th>KEY</th><th>ACTION</th>";
|
||
for (let key in shortcuts) {
|
||
html += `<tr><td>${key}</td><td>${shortcuts[key]}</td></tr>`;
|
||
}
|
||
|
||
// Add custom key bindings that have associated descriptions
|
||
for (let binding in bindings) {
|
||
if (bindings[binding].key && bindings[binding].description) {
|
||
html += `<tr><td>${bindings[binding].key}</td><td>${bindings[binding].description}</td></tr>`;
|
||
}
|
||
}
|
||
|
||
html += "</table>";
|
||
|
||
dom.overlay.innerHTML = `
|
||
<header>
|
||
<a class="close" href="#"><span class="icon"></span></a>
|
||
</header>
|
||
<div class="viewport">
|
||
<div class="viewport-inner">${html}</div>
|
||
</div>
|
||
`;
|
||
|
||
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;
|
||
}
|