move fragments to separate controller
This commit is contained in:
302
js/controllers/fragments.js
Normal file
302
js/controllers/fragments.js
Normal file
@ -0,0 +1,302 @@
|
||||
import { extend, toArray } from '../utils/util.js'
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export default class Fragments {
|
||||
|
||||
constructor( Reveal ) {
|
||||
|
||||
this.Reveal = Reveal;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows all fragments in the presentation. Used when
|
||||
* fragments are disabled presentation-wide.
|
||||
*/
|
||||
showAll() {
|
||||
|
||||
toArray( this.Reveal.getSlidesElement().querySelectorAll( '.fragment' ) ).forEach( element => {
|
||||
element.classList.add( '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' );
|
||||
let hiddenFragments = currentSlide.querySelectorAll( '.fragment: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 = toArray( 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 = toArray( horizontalSlide.querySelectorAll( '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 );
|
||||
}
|
||||
}
|
||||
|
||||
toArray( 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 ) {
|
||||
if( !el.classList.contains( 'visible' ) ) changedFragments.shown.push( el );
|
||||
el.classList.add( 'visible' );
|
||||
el.classList.remove( 'current-fragment' );
|
||||
|
||||
// Announce the fragments one by one to the Screen Reader
|
||||
this.Reveal.announceStatus( this.Reveal.getStatusText( el ) );
|
||||
|
||||
if( i === index ) {
|
||||
el.classList.add( 'current-fragment' );
|
||||
this.Reveal.slideContent.startEmbeddedContent( el );
|
||||
}
|
||||
}
|
||||
// Hidden fragments
|
||||
else {
|
||||
if( el.classList.contains( 'visible' ) ) changedFragments.hidden.push( el );
|
||||
el.classList.remove( 'visible' );
|
||||
el.classList.remove( 'current-fragment' );
|
||||
}
|
||||
|
||||
} );
|
||||
|
||||
// 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;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 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' ) );
|
||||
if( fragments.length ) {
|
||||
|
||||
// If no index is specified, find the current
|
||||
if( typeof index !== 'number' ) {
|
||||
let lastVisibleFragment = this.sort( currentSlide.querySelectorAll( '.fragment.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( 'fragmenthidden', { fragment: changedFragments.hidden[0], fragments: changedFragments.hidden } );
|
||||
}
|
||||
|
||||
if( changedFragments.shown.length ) {
|
||||
this.Reveal.dispatchEvent( 'fragmentshown', { fragment: changedFragments.shown[0], fragments: changedFragments.shown } );
|
||||
}
|
||||
|
||||
this.Reveal.updateControls();
|
||||
this.Reveal.updateProgress();
|
||||
|
||||
if( this.Reveal.getConfig().fragmentInURL ) {
|
||||
this.Reveal.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 );
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import { HORIZONTAL_SLIDES_SELECTOR, VERTICAL_SLIDES_SELECTOR } from '../utils/constants.js'
|
||||
import { extend, toArray, closestParent } from '../utils/util.js'
|
||||
import { isMobile } from '../utils/device.js'
|
||||
|
||||
/**
|
||||
* Handles loading, unloading and playback of slide
|
||||
@ -13,6 +14,26 @@ export default class SlideContent {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Should the given element be preloaded?
|
||||
* Decides based on local element attributes and global config.
|
||||
*
|
||||
* @param {HTMLElement} element
|
||||
*/
|
||||
shouldPreload( element ) {
|
||||
|
||||
// Prefer an explicit global preload setting
|
||||
let preload = this.Reveal.getConfig().preloadIframes;
|
||||
|
||||
// If no global setting is available, fall back on the element's
|
||||
// own preload setting
|
||||
if( typeof preload !== 'boolean' ) {
|
||||
preload = element.hasAttribute( 'data-preload' );
|
||||
}
|
||||
|
||||
return preload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the given slide is within the configured view
|
||||
* distance. Shows the slide element and loads any content
|
||||
@ -27,7 +48,7 @@ export default class SlideContent {
|
||||
|
||||
// Media elements with data-src attributes
|
||||
toArray( slide.querySelectorAll( 'img[data-src], video[data-src], audio[data-src], iframe[data-src]' ) ).forEach( element => {
|
||||
if( element.tagName !== 'IFRAME' || shouldPreload( element ) ) {
|
||||
if( element.tagName !== 'IFRAME' || this.shouldPreload( element ) ) {
|
||||
element.setAttribute( 'src', element.getAttribute( 'data-src' ) );
|
||||
element.setAttribute( 'data-lazy-loaded', '' );
|
||||
element.removeAttribute( 'data-src' );
|
||||
@ -89,7 +110,7 @@ export default class SlideContent {
|
||||
// Inline video playback works (at least in Mobile Safari) as
|
||||
// long as the video is muted and the `playsinline` attribute is
|
||||
// present
|
||||
if( isMobileDevice ) {
|
||||
if( isMobile ) {
|
||||
video.muted = true;
|
||||
video.autoplay = true;
|
||||
video.setAttribute( 'playsinline', '' );
|
||||
@ -126,7 +147,7 @@ export default class SlideContent {
|
||||
if( backgroundIframeElement ) {
|
||||
|
||||
// Check if this iframe is eligible to be preloaded
|
||||
if( shouldPreload( background ) && !/autoplay=(1|true|yes)/gi.test( backgroundIframe ) ) {
|
||||
if( this.shouldPreload( background ) && !/autoplay=(1|true|yes)/gi.test( backgroundIframe ) ) {
|
||||
if( backgroundIframeElement.getAttribute( 'src' ) !== backgroundIframe ) {
|
||||
backgroundIframeElement.setAttribute( 'src', backgroundIframe );
|
||||
}
|
||||
@ -222,7 +243,7 @@ export default class SlideContent {
|
||||
}
|
||||
|
||||
// Prefer an explicit global autoplay setting
|
||||
let autoplay = config.autoPlayMedia;
|
||||
let autoplay = this.Reveal.getConfig().autoPlayMedia;
|
||||
|
||||
// If no global setting is available, fall back on the element's
|
||||
// own autoplay setting
|
||||
@ -238,7 +259,7 @@ export default class SlideContent {
|
||||
}
|
||||
// Mobile devices never fire a loaded event so instead
|
||||
// of waiting, we initiate playback
|
||||
else if( isMobileDevice ) {
|
||||
else if( isMobile ) {
|
||||
let promise = el.play();
|
||||
|
||||
// If autoplay does not work, ensure that the controls are visible so
|
||||
@ -327,7 +348,7 @@ export default class SlideContent {
|
||||
if( isAttachedToDOM && isVisible ) {
|
||||
|
||||
// Prefer an explicit global autoplay setting
|
||||
let autoplay = config.autoPlayMedia;
|
||||
let autoplay = this.Reveal.getConfig().autoPlayMedia;
|
||||
|
||||
// If no global setting is available, fall back on the element's
|
||||
// own autoplay setting
|
||||
|
Reference in New Issue
Block a user