279 lines
9.6 KiB
JavaScript
279 lines
9.6 KiB
JavaScript
|
import { queryAll, enterFullscreen } from '../utils/util.js'
|
||
|
import { isAndroid } from '../utils/device.js'
|
||
|
|
||
|
/**
|
||
|
* 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
|
||
|
* - .enter-fullscreen
|
||
|
*/
|
||
|
export default class Controls {
|
||
|
|
||
|
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 );
|
||
|
this.onEnterFullscreen = this.onEnterFullscreen.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>
|
||
|
<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>`;
|
||
|
|
||
|
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' );
|
||
|
this.controlsFullscreen = queryAll( revealElement, '.enter-fullscreen' );
|
||
|
|
||
|
// 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 ) );
|
||
|
this.controlsFullscreen.forEach( el => el.addEventListener( eventName, this.onEnterFullscreen, 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 ) );
|
||
|
this.controlsFullscreen.forEach( el => el.removeEventListener( eventName, this.onEnterFullscreen, 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();
|
||
|
|
||
|
}
|
||
|
|
||
|
onEnterFullscreen( event ) {
|
||
|
|
||
|
const config = this.Reveal.getConfig();
|
||
|
const viewport = this.Reveal.getViewportElement();
|
||
|
|
||
|
enterFullscreen( config.embedded ? viewport : viewport.parentElement );
|
||
|
|
||
|
}
|
||
|
|
||
|
}
|