deployment-talks/slides/js/controllers/fragments.js

375 lines
9.1 KiB
JavaScript

import { extend, queryAll } from '../utils/util.js'
/**
* Handles sorting and navigation of slide fragments.
* Fragments are elements within a slide that are
* revealed/animated incrementally.
*/
export default class Fragments {
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, slide = this.Reveal.getCurrentSlide() ) {
let changedFragments = {
shown: [],
hidden: []
};
if( slide && this.Reveal.getConfig().fragments ) {
fragments = fragments || this.sort( slide.querySelectorAll( '.fragment' ) );
if( fragments.length ) {
let maxIndex = 0;
if( typeof index !== 'number' ) {
let currentFragment = this.sort( slide.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 );
slide.setAttribute( 'data-fragment', index );
}
}
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
}
});
}
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 );
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 );
}
}