controls.js 9.9 KB

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