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