1
0
Fork 0
why-cant-we-deploy-today/js/controllers/fragments.js

351 lines
10 KiB
JavaScript
Raw Normal View History

2023-05-23 19:50:14 +02:00
import { extend, queryAll } from "../utils/util.js";
2020-03-09 18:51:07 +01:00
/**
2020-03-09 20:57:12 +01:00
* Handles sorting and navigation of slide fragments.
* Fragments are elements within a slide that are
* revealed/animated incrementally.
2020-03-09 18:51:07 +01:00
*/
export default class Fragments {
2023-05-23 19:50:14 +02:00
constructor(Reveal) {
this.Reveal = Reveal;
}
/**
* Called when the reveal.js config is updated.
*/
configure(config, oldConfig) {
if (config.fragments === false) {
this.disable();
} else if (oldConfig.fragments === false) {
this.enable();
}
}
/**
* If fragments are disabled in the deck, they should all be
* visible rather than stepped through.
*/
disable() {
queryAll(this.Reveal.getSlidesElement(), ".fragment").forEach((element) => {
element.classList.add("visible");
element.classList.remove("current-fragment");
});
}
/**
* Reverse of #disable(). Only called if fragments have
* previously been disabled.
*/
enable() {
queryAll(this.Reveal.getSlidesElement(), ".fragment").forEach((element) => {
element.classList.remove("visible");
element.classList.remove("current-fragment");
});
}
/**
* Returns an object describing the available fragment
* directions.
*
* @return {{prev: boolean, next: boolean}}
*/
availableRoutes() {
let currentSlide = this.Reveal.getCurrentSlide();
if (currentSlide && this.Reveal.getConfig().fragments) {
let fragments = currentSlide.querySelectorAll(".fragment:not(.disabled)");
let hiddenFragments = currentSlide.querySelectorAll(
".fragment:not(.disabled):not(.visible)"
);
return {
prev: fragments.length - hiddenFragments.length > 0,
next: !!hiddenFragments.length,
};
} else {
return { prev: false, next: false };
}
}
/**
* Return a sorted fragments list, ordered by an increasing
* "data-fragment-index" attribute.
*
* Fragments will be revealed in the order that they are returned by
* this function, so you can use the index attributes to control the
* order of fragment appearance.
*
* To maintain a sensible default fragment order, fragments are presumed
* to be passed in document order. This function adds a "fragment-index"
* attribute to each node if such an attribute is not already present,
* and sets that attribute to an integer value which is the position of
* the fragment within the fragments list.
*
* @param {object[]|*} fragments
* @param {boolean} grouped If true the returned array will contain
* nested arrays for all fragments with the same index
* @return {object[]} sorted Sorted array of fragments
*/
sort(fragments, grouped = false) {
fragments = Array.from(fragments);
let ordered = [],
unordered = [],
sorted = [];
// Group ordered and unordered elements
fragments.forEach((fragment) => {
if (fragment.hasAttribute("data-fragment-index")) {
let index = parseInt(fragment.getAttribute("data-fragment-index"), 10);
if (!ordered[index]) {
ordered[index] = [];
}
ordered[index].push(fragment);
} else {
unordered.push([fragment]);
}
});
// Append fragments without explicit indices in their
// DOM order
ordered = ordered.concat(unordered);
// Manually count the index up per group to ensure there
// are no gaps
let index = 0;
// Push all fragments in their sorted order to an array,
// this flattens the groups
ordered.forEach((group) => {
group.forEach((fragment) => {
sorted.push(fragment);
fragment.setAttribute("data-fragment-index", index);
});
index++;
});
return grouped === true ? ordered : sorted;
}
/**
* Sorts and formats all of fragments in the
* presentation.
*/
sortAll() {
this.Reveal.getHorizontalSlides().forEach((horizontalSlide) => {
let verticalSlides = queryAll(horizontalSlide, "section");
verticalSlides.forEach((verticalSlide, y) => {
this.sort(verticalSlide.querySelectorAll(".fragment"));
}, this);
if (verticalSlides.length === 0)
this.sort(horizontalSlide.querySelectorAll(".fragment"));
});
}
/**
* Refreshes the fragments on the current slide so that they
* have the appropriate classes (.visible + .current-fragment).
*
* @param {number} [index] The index of the current fragment
* @param {array} [fragments] Array containing all fragments
* in the current slide
*
* @return {{shown: array, hidden: array}}
*/
update(index, fragments) {
let changedFragments = {
shown: [],
hidden: [],
};
let currentSlide = this.Reveal.getCurrentSlide();
if (currentSlide && this.Reveal.getConfig().fragments) {
fragments =
fragments || this.sort(currentSlide.querySelectorAll(".fragment"));
if (fragments.length) {
let maxIndex = 0;
if (typeof index !== "number") {
let currentFragment = this.sort(
currentSlide.querySelectorAll(".fragment.visible")
).pop();
if (currentFragment) {
index = parseInt(
currentFragment.getAttribute("data-fragment-index") || 0,
10
);
}
}
Array.from(fragments).forEach((el, i) => {
if (el.hasAttribute("data-fragment-index")) {
i = parseInt(el.getAttribute("data-fragment-index"), 10);
}
maxIndex = Math.max(maxIndex, i);
// Visible fragments
if (i <= index) {
let wasVisible = el.classList.contains("visible");
el.classList.add("visible");
el.classList.remove("current-fragment");
if (i === index) {
// Announce the fragments one by one to the Screen Reader
this.Reveal.announceStatus(this.Reveal.getStatusText(el));
el.classList.add("current-fragment");
this.Reveal.slideContent.startEmbeddedContent(el);
}
if (!wasVisible) {
changedFragments.shown.push(el);
this.Reveal.dispatchEvent({
target: el,
type: "visible",
bubbles: false,
});
}
}
// Hidden fragments
else {
let wasVisible = el.classList.contains("visible");
el.classList.remove("visible");
el.classList.remove("current-fragment");
if (wasVisible) {
this.Reveal.slideContent.stopEmbeddedContent(el);
changedFragments.hidden.push(el);
this.Reveal.dispatchEvent({
target: el,
type: "hidden",
bubbles: false,
});
}
}
});
// Write the current fragment index to the slide <section>.
// This can be used by end users to apply styles based on
// the current fragment index.
index = typeof index === "number" ? index : -1;
index = Math.max(Math.min(index, maxIndex), -1);
currentSlide.setAttribute("data-fragment", index);
}
}
return changedFragments;
}
/**
* Formats the fragments on the given slide so that they have
* valid indices. Call this if fragments are changed in the DOM
* after reveal.js has already initialized.
*
* @param {HTMLElement} slide
* @return {Array} a list of the HTML fragments that were synced
*/
sync(slide = this.Reveal.getCurrentSlide()) {
return this.sort(slide.querySelectorAll(".fragment"));
}
/**
* Navigate to the specified slide fragment.
*
* @param {?number} index The index of the fragment that
* should be shown, -1 means all are invisible
* @param {number} offset Integer offset to apply to the
* fragment index
*
* @return {boolean} true if a change was made in any
* fragments visibility as part of this call
*/
goto(index, offset = 0) {
let currentSlide = this.Reveal.getCurrentSlide();
if (currentSlide && this.Reveal.getConfig().fragments) {
let fragments = this.sort(
currentSlide.querySelectorAll(".fragment:not(.disabled)")
);
if (fragments.length) {
// If no index is specified, find the current
if (typeof index !== "number") {
let lastVisibleFragment = this.sort(
currentSlide.querySelectorAll(".fragment:not(.disabled).visible")
).pop();
if (lastVisibleFragment) {
index = parseInt(
lastVisibleFragment.getAttribute("data-fragment-index") || 0,
10
);
} else {
index = -1;
}
}
// Apply the offset if there is one
index += offset;
let changedFragments = this.update(index, fragments);
if (changedFragments.hidden.length) {
this.Reveal.dispatchEvent({
type: "fragmenthidden",
data: {
fragment: changedFragments.hidden[0],
fragments: changedFragments.hidden,
},
});
}
if (changedFragments.shown.length) {
this.Reveal.dispatchEvent({
type: "fragmentshown",
data: {
fragment: changedFragments.shown[0],
fragments: changedFragments.shown,
},
});
}
this.Reveal.controls.update();
this.Reveal.progress.update();
if (this.Reveal.getConfig().fragmentInURL) {
this.Reveal.location.writeURL();
}
return !!(
changedFragments.shown.length || changedFragments.hidden.length
);
}
}
return false;
}
/**
* Navigate to the next slide fragment.
*
* @return {boolean} true if there was a next fragment,
* false otherwise
*/
next() {
return this.goto(null, 1);
}
/**
* Navigate to the previous slide fragment.
*
* @return {boolean} true if there was a previous fragment,
* false otherwise
*/
prev() {
return this.goto(null, -1);
}
}