2023-05-23 19:50:14 +02:00
|
|
|
import { queryAll } from "../utils/util.js";
|
|
|
|
import { isAndroid } from "../utils/device.js";
|
2020-03-16 13:53:23 +01:00
|
|
|
|
|
|
|
/**
|
2020-03-16 14:30:36 +01:00
|
|
|
* 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
|
2020-03-16 13:53:23 +01:00
|
|
|
*/
|
|
|
|
export default class Controls {
|
2023-05-23 19:50:14 +02:00
|
|
|
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 = `<button class="navigate-left" aria-label="${
|
|
|
|
rtl ? "next slide" : "previous slide"
|
|
|
|
}"><div class="controls-arrow"></div></button>
|
|
|
|
<button class="navigate-right" aria-label="${
|
|
|
|
rtl ? "previous slide" : "next slide"
|
|
|
|
}"><div class="controls-arrow"></div></button>
|
2020-03-16 13:53:23 +01:00
|
|
|
<button class="navigate-up" aria-label="above slide"><div class="controls-arrow"></div></button>
|
|
|
|
<button class="navigate-down" aria-label="below slide"><div class="controls-arrow"></div></button>`;
|
|
|
|
|
2023-05-23 19:50:14 +02:00
|
|
|
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();
|
|
|
|
}
|
|
|
|
}
|