import { queryAll } from "../utils/util.js"; import { isAndroid } from "../utils/device.js"; /** * Manages our presentation controls. This includes both * the built-in control arrows as well as event monitoring * of any elements within the presentation with either of the * following helper classes: * - .navigate-up * - .navigate-right * - .navigate-down * - .navigate-left * - .navigate-next * - .navigate-prev */ export default class Controls { constructor(Reveal) { this.Reveal = Reveal; this.onNavigateLeftClicked = this.onNavigateLeftClicked.bind(this); this.onNavigateRightClicked = this.onNavigateRightClicked.bind(this); this.onNavigateUpClicked = this.onNavigateUpClicked.bind(this); this.onNavigateDownClicked = this.onNavigateDownClicked.bind(this); this.onNavigatePrevClicked = this.onNavigatePrevClicked.bind(this); this.onNavigateNextClicked = this.onNavigateNextClicked.bind(this); } render() { const rtl = this.Reveal.getConfig().rtl; const revealElement = this.Reveal.getRevealElement(); this.element = document.createElement("aside"); this.element.className = "controls"; this.element.innerHTML = ` `; this.Reveal.getRevealElement().appendChild(this.element); // There can be multiple instances of controls throughout the page this.controlsLeft = queryAll(revealElement, ".navigate-left"); this.controlsRight = queryAll(revealElement, ".navigate-right"); this.controlsUp = queryAll(revealElement, ".navigate-up"); this.controlsDown = queryAll(revealElement, ".navigate-down"); this.controlsPrev = queryAll(revealElement, ".navigate-prev"); this.controlsNext = queryAll(revealElement, ".navigate-next"); // The left, right and down arrows in the standard reveal.js controls this.controlsRightArrow = this.element.querySelector(".navigate-right"); this.controlsLeftArrow = this.element.querySelector(".navigate-left"); this.controlsDownArrow = this.element.querySelector(".navigate-down"); } /** * Called when the reveal.js config is updated. */ configure(config, oldConfig) { this.element.style.display = config.controls ? "block" : "none"; this.element.setAttribute("data-controls-layout", config.controlsLayout); this.element.setAttribute( "data-controls-back-arrows", config.controlsBackArrows ); } bind() { // Listen to both touch and click events, in case the device // supports both let pointerEvents = ["touchstart", "click"]; // Only support touch for Android, fixes double navigations in // stock browser if (isAndroid) { pointerEvents = ["touchstart"]; } pointerEvents.forEach((eventName) => { this.controlsLeft.forEach((el) => el.addEventListener(eventName, this.onNavigateLeftClicked, false) ); this.controlsRight.forEach((el) => el.addEventListener(eventName, this.onNavigateRightClicked, false) ); this.controlsUp.forEach((el) => el.addEventListener(eventName, this.onNavigateUpClicked, false) ); this.controlsDown.forEach((el) => el.addEventListener(eventName, this.onNavigateDownClicked, false) ); this.controlsPrev.forEach((el) => el.addEventListener(eventName, this.onNavigatePrevClicked, false) ); this.controlsNext.forEach((el) => el.addEventListener(eventName, this.onNavigateNextClicked, false) ); }); } unbind() { ["touchstart", "click"].forEach((eventName) => { this.controlsLeft.forEach((el) => el.removeEventListener(eventName, this.onNavigateLeftClicked, false) ); this.controlsRight.forEach((el) => el.removeEventListener(eventName, this.onNavigateRightClicked, false) ); this.controlsUp.forEach((el) => el.removeEventListener(eventName, this.onNavigateUpClicked, false) ); this.controlsDown.forEach((el) => el.removeEventListener(eventName, this.onNavigateDownClicked, false) ); this.controlsPrev.forEach((el) => el.removeEventListener(eventName, this.onNavigatePrevClicked, false) ); this.controlsNext.forEach((el) => el.removeEventListener(eventName, this.onNavigateNextClicked, false) ); }); } /** * Updates the state of all control/navigation arrows. */ update() { let routes = this.Reveal.availableRoutes(); // Remove the 'enabled' class from all directions [ ...this.controlsLeft, ...this.controlsRight, ...this.controlsUp, ...this.controlsDown, ...this.controlsPrev, ...this.controlsNext, ].forEach((node) => { node.classList.remove("enabled", "fragmented"); // Set 'disabled' attribute on all directions node.setAttribute("disabled", "disabled"); }); // Add the 'enabled' class to the available routes; remove 'disabled' attribute to enable buttons if (routes.left) this.controlsLeft.forEach((el) => { el.classList.add("enabled"); el.removeAttribute("disabled"); }); if (routes.right) this.controlsRight.forEach((el) => { el.classList.add("enabled"); el.removeAttribute("disabled"); }); if (routes.up) this.controlsUp.forEach((el) => { el.classList.add("enabled"); el.removeAttribute("disabled"); }); if (routes.down) this.controlsDown.forEach((el) => { el.classList.add("enabled"); el.removeAttribute("disabled"); }); // Prev/next buttons if (routes.left || routes.up) this.controlsPrev.forEach((el) => { el.classList.add("enabled"); el.removeAttribute("disabled"); }); if (routes.right || routes.down) this.controlsNext.forEach((el) => { el.classList.add("enabled"); el.removeAttribute("disabled"); }); // Highlight fragment directions let currentSlide = this.Reveal.getCurrentSlide(); if (currentSlide) { let fragmentsRoutes = this.Reveal.fragments.availableRoutes(); // Always apply fragment decorator to prev/next buttons if (fragmentsRoutes.prev) this.controlsPrev.forEach((el) => { el.classList.add("fragmented", "enabled"); el.removeAttribute("disabled"); }); if (fragmentsRoutes.next) this.controlsNext.forEach((el) => { el.classList.add("fragmented", "enabled"); el.removeAttribute("disabled"); }); // Apply fragment decorators to directional buttons based on // what slide axis they are in if (this.Reveal.isVerticalSlide(currentSlide)) { if (fragmentsRoutes.prev) this.controlsUp.forEach((el) => { el.classList.add("fragmented", "enabled"); el.removeAttribute("disabled"); }); if (fragmentsRoutes.next) this.controlsDown.forEach((el) => { el.classList.add("fragmented", "enabled"); el.removeAttribute("disabled"); }); } else { if (fragmentsRoutes.prev) this.controlsLeft.forEach((el) => { el.classList.add("fragmented", "enabled"); el.removeAttribute("disabled"); }); if (fragmentsRoutes.next) this.controlsRight.forEach((el) => { el.classList.add("fragmented", "enabled"); el.removeAttribute("disabled"); }); } } if (this.Reveal.getConfig().controlsTutorial) { let indices = this.Reveal.getIndices(); // Highlight control arrows with an animation to ensure // that the viewer knows how to navigate if (!this.Reveal.hasNavigatedVertically() && routes.down) { this.controlsDownArrow.classList.add("highlight"); } else { this.controlsDownArrow.classList.remove("highlight"); if (this.Reveal.getConfig().rtl) { if ( !this.Reveal.hasNavigatedHorizontally() && routes.left && indices.v === 0 ) { this.controlsLeftArrow.classList.add("highlight"); } else { this.controlsLeftArrow.classList.remove("highlight"); } } else { if ( !this.Reveal.hasNavigatedHorizontally() && routes.right && indices.v === 0 ) { this.controlsRightArrow.classList.add("highlight"); } else { this.controlsRightArrow.classList.remove("highlight"); } } } } } destroy() { this.unbind(); this.element.remove(); } /** * Event handlers for navigation control buttons. */ onNavigateLeftClicked(event) { event.preventDefault(); this.Reveal.onUserInput(); if (this.Reveal.getConfig().navigationMode === "linear") { this.Reveal.prev(); } else { this.Reveal.left(); } } onNavigateRightClicked(event) { event.preventDefault(); this.Reveal.onUserInput(); if (this.Reveal.getConfig().navigationMode === "linear") { this.Reveal.next(); } else { this.Reveal.right(); } } onNavigateUpClicked(event) { event.preventDefault(); this.Reveal.onUserInput(); this.Reveal.up(); } onNavigateDownClicked(event) { event.preventDefault(); this.Reveal.onUserInput(); this.Reveal.down(); } onNavigatePrevClicked(event) { event.preventDefault(); this.Reveal.onUserInput(); this.Reveal.prev(); } onNavigateNextClicked(event) { event.preventDefault(); this.Reveal.onUserInput(); this.Reveal.next(); } }