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();
}
}