controls.js 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. import { toArray } from '../utils/util.js'
  2. import { isMobile, isAndroid } from '../utils/device.js'
  3. /**
  4. *
  5. */
  6. export default class Controls {
  7. constructor( Reveal ) {
  8. this.Reveal = Reveal;
  9. this.onNavigateLeftClicked = this.onNavigateLeftClicked.bind( this );
  10. this.onNavigateRightClicked = this.onNavigateRightClicked.bind( this );
  11. this.onNavigateUpClicked = this.onNavigateUpClicked.bind( this );
  12. this.onNavigateDownClicked = this.onNavigateDownClicked.bind( this );
  13. this.onNavigatePrevClicked = this.onNavigatePrevClicked.bind( this );
  14. this.onNavigateNextClicked = this.onNavigateNextClicked.bind( this );
  15. }
  16. render() {
  17. const rtl = this.Reveal.getConfig().rtl;
  18. const revealElement = this.Reveal.getRevealElement();
  19. this.element = document.createElement( 'aside' );
  20. this.element.className = 'controls';
  21. this.element.innerHTML =
  22. `<button class="navigate-left" aria-label="${ rtl ? 'next slide' : 'previous slide' }"><div class="controls-arrow"></div></button>
  23. <button class="navigate-right" aria-label="${ rtl ? 'previous slide' : 'next slide' }"><div class="controls-arrow"></div></button>
  24. <button class="navigate-up" aria-label="above slide"><div class="controls-arrow"></div></button>
  25. <button class="navigate-down" aria-label="below slide"><div class="controls-arrow"></div></button>`;
  26. this.Reveal.getRevealElement().appendChild( this.element );
  27. // There can be multiple instances of controls throughout the page
  28. this.controlsLeft = toArray( revealElement.querySelectorAll( '.navigate-left' ) );
  29. this.controlsRight = toArray( revealElement.querySelectorAll( '.navigate-right' ) );
  30. this.controlsUp = toArray( revealElement.querySelectorAll( '.navigate-up' ) );
  31. this.controlsDown = toArray( revealElement.querySelectorAll( '.navigate-down' ) );
  32. this.controlsPrev = toArray( revealElement.querySelectorAll( '.navigate-prev' ) );
  33. this.controlsNext = toArray( revealElement.querySelectorAll( '.navigate-next' ) );
  34. // The left, right and down arrows in the standard reveal.js controls
  35. this.controlsRightArrow = this.element.querySelector( '.navigate-right' );
  36. this.controlsLeftArrow = this.element.querySelector( '.navigate-left' );
  37. this.controlsDownArrow = this.element.querySelector( '.navigate-down' );
  38. }
  39. /**
  40. * Called when the reveal.js config is updated.
  41. */
  42. configure( config, oldConfig ) {
  43. this.element.style.display = config.controls ? 'block' : 'none';
  44. this.element.setAttribute( 'data-controls-layout', config.controlsLayout );
  45. this.element.setAttribute( 'data-controls-back-arrows', config.controlsBackArrows );
  46. }
  47. bind() {
  48. // Listen to both touch and click events, in case the device
  49. // supports both
  50. let pointerEvents = [ 'touchstart', 'click' ];
  51. // Only support touch for Android, fixes double navigations in
  52. // stock browser
  53. if( isAndroid ) {
  54. pointerEvents = [ 'touchstart' ];
  55. }
  56. pointerEvents.forEach( eventName => {
  57. this.controlsLeft.forEach( el => el.addEventListener( eventName, this.onNavigateLeftClicked, false ) );
  58. this.controlsRight.forEach( el => el.addEventListener( eventName, this.onNavigateRightClicked, false ) );
  59. this.controlsUp.forEach( el => el.addEventListener( eventName, this.onNavigateUpClicked, false ) );
  60. this.controlsDown.forEach( el => el.addEventListener( eventName, this.onNavigateDownClicked, false ) );
  61. this.controlsPrev.forEach( el => el.addEventListener( eventName, this.onNavigatePrevClicked, false ) );
  62. this.controlsNext.forEach( el => el.addEventListener( eventName, this.onNavigateNextClicked, false ) );
  63. } );
  64. }
  65. unbind() {
  66. [ 'touchstart', 'click' ].forEach( eventName => {
  67. this.controlsLeft.forEach( el => el.removeEventListener( eventName, this.onNavigateLeftClicked, false ) );
  68. this.controlsRight.forEach( el => el.removeEventListener( eventName, this.onNavigateRightClicked, false ) );
  69. this.controlsUp.forEach( el => el.removeEventListener( eventName, this.onNavigateUpClicked, false ) );
  70. this.controlsDown.forEach( el => el.removeEventListener( eventName, this.onNavigateDownClicked, false ) );
  71. this.controlsPrev.forEach( el => el.removeEventListener( eventName, this.onNavigatePrevClicked, false ) );
  72. this.controlsNext.forEach( el => el.removeEventListener( eventName, this.onNavigateNextClicked, false ) );
  73. } );
  74. }
  75. /**
  76. * Updates the state of all control/navigation arrows.
  77. */
  78. update() {
  79. let routes = this.Reveal.availableRoutes();
  80. // Remove the 'enabled' class from all directions
  81. [...this.controlsLeft, ...this.controlsRight, ...this.controlsUp, ...this.controlsDown, ...this.controlsPrev, ...this.controlsNext].forEach( node => {
  82. node.classList.remove( 'enabled', 'fragmented' );
  83. // Set 'disabled' attribute on all directions
  84. node.setAttribute( 'disabled', 'disabled' );
  85. } );
  86. // Add the 'enabled' class to the available routes; remove 'disabled' attribute to enable buttons
  87. if( routes.left ) this.controlsLeft.forEach( el => { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
  88. if( routes.right ) this.controlsRight.forEach( el => { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
  89. if( routes.up ) this.controlsUp.forEach( el => { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
  90. if( routes.down ) this.controlsDown.forEach( el => { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
  91. // Prev/next buttons
  92. if( routes.left || routes.up ) this.controlsPrev.forEach( el => { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
  93. if( routes.right || routes.down ) this.controlsNext.forEach( el => { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
  94. // Highlight fragment directions
  95. let currentSlide = this.Reveal.getCurrentSlide();
  96. if( currentSlide ) {
  97. let fragmentsRoutes = this.Reveal.fragments.availableRoutes();
  98. // Always apply fragment decorator to prev/next buttons
  99. if( fragmentsRoutes.prev ) this.controlsPrev.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
  100. if( fragmentsRoutes.next ) this.controlsNext.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
  101. // Apply fragment decorators to directional buttons based on
  102. // what slide axis they are in
  103. if( this.Reveal.isVerticalSlide( currentSlide ) ) {
  104. if( fragmentsRoutes.prev ) this.controlsUp.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
  105. if( fragmentsRoutes.next ) this.controlsDown.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
  106. }
  107. else {
  108. if( fragmentsRoutes.prev ) this.controlsLeft.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
  109. if( fragmentsRoutes.next ) this.controlsRight.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
  110. }
  111. }
  112. if( this.Reveal.getConfig().controlsTutorial ) {
  113. let indices = this.Reveal.getIndices();
  114. // Highlight control arrows with an animation to ensure
  115. // that the viewer knows how to navigate
  116. if( !this.Reveal.hasNavigatedVertically() && routes.down ) {
  117. this.controlsDownArrow.classList.add( 'highlight' );
  118. }
  119. else {
  120. this.controlsDownArrow.classList.remove( 'highlight' );
  121. if( this.Reveal.getConfig().rtl ) {
  122. if( !this.Reveal.hasNavigatedHorizontally() && routes.left && indices.v === 0 ) {
  123. this.controlsLeftArrow.classList.add( 'highlight' );
  124. }
  125. else {
  126. this.controlsLeftArrow.classList.remove( 'highlight' );
  127. }
  128. } else {
  129. if( !this.Reveal.hasNavigatedHorizontally() && routes.right && indices.v === 0 ) {
  130. this.controlsRightArrow.classList.add( 'highlight' );
  131. }
  132. else {
  133. this.controlsRightArrow.classList.remove( 'highlight' );
  134. }
  135. }
  136. }
  137. }
  138. }
  139. /**
  140. * Event handlers for navigation control buttons.
  141. */
  142. onNavigateLeftClicked( event ) {
  143. event.preventDefault();
  144. this.Reveal.onUserInput();
  145. if( this.Reveal.getConfig().navigationMode === 'linear' ) {
  146. this.Reveal.prev();
  147. }
  148. else {
  149. this.Reveal.left();
  150. }
  151. }
  152. onNavigateRightClicked( event ) {
  153. event.preventDefault();
  154. this.Reveal.onUserInput();
  155. if( this.Reveal.getConfig().navigationMode === 'linear' ) {
  156. this.Reveal.next();
  157. }
  158. else {
  159. this.Reveal.right();
  160. }
  161. }
  162. onNavigateUpClicked( event ) {
  163. event.preventDefault();
  164. this.Reveal.onUserInput();
  165. this.Reveal.up();
  166. }
  167. onNavigateDownClicked( event ) {
  168. event.preventDefault();
  169. this.Reveal.onUserInput();
  170. this.Reveal.down();
  171. }
  172. onNavigatePrevClicked( event ) {
  173. event.preventDefault();
  174. this.Reveal.onUserInput();
  175. this.Reveal.prev();
  176. }
  177. onNavigateNextClicked( event ) {
  178. event.preventDefault();
  179. this.Reveal.onUserInput();
  180. this.Reveal.next();
  181. }
  182. }