controls.js 8.9 KB

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