| import { queryAll } 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 |
| */ |
| 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 ); |
| |
| } |
| |
| 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' ); |
| |
| // 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 ) ); |
| } ); |
| |
| } |
| |
| 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 ) ); |
| } ); |
| |
| } |
| |
| /** |
| * 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(); |
| |
| } |
| |
| |
| } |