controls.js 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  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 = config.controls ? 'block' : 'none';
  56. this.element.setAttribute( 'data-controls-layout', config.controlsLayout );
  57. this.element.setAttribute( 'data-controls-back-arrows', config.controlsBackArrows );
  58. }
  59. bind() {
  60. // Listen to both touch and click events, in case the device
  61. // supports both
  62. let pointerEvents = [ 'touchstart', 'click' ];
  63. // Only support touch for Android, fixes double navigations in
  64. // stock browser
  65. if( isAndroid ) {
  66. pointerEvents = [ 'touchstart' ];
  67. }
  68. pointerEvents.forEach( eventName => {
  69. this.controlsLeft.forEach( el => el.addEventListener( eventName, this.onNavigateLeftClicked, false ) );
  70. this.controlsRight.forEach( el => el.addEventListener( eventName, this.onNavigateRightClicked, false ) );
  71. this.controlsUp.forEach( el => el.addEventListener( eventName, this.onNavigateUpClicked, false ) );
  72. this.controlsDown.forEach( el => el.addEventListener( eventName, this.onNavigateDownClicked, false ) );
  73. this.controlsPrev.forEach( el => el.addEventListener( eventName, this.onNavigatePrevClicked, false ) );
  74. this.controlsNext.forEach( el => el.addEventListener( eventName, this.onNavigateNextClicked, false ) );
  75. this.controlsFullscreen.forEach( el => el.addEventListener( eventName, this.onEnterFullscreen, false ) );
  76. } );
  77. }
  78. unbind() {
  79. [ 'touchstart', 'click' ].forEach( eventName => {
  80. this.controlsLeft.forEach( el => el.removeEventListener( eventName, this.onNavigateLeftClicked, false ) );
  81. this.controlsRight.forEach( el => el.removeEventListener( eventName, this.onNavigateRightClicked, false ) );
  82. this.controlsUp.forEach( el => el.removeEventListener( eventName, this.onNavigateUpClicked, false ) );
  83. this.controlsDown.forEach( el => el.removeEventListener( eventName, this.onNavigateDownClicked, false ) );
  84. this.controlsPrev.forEach( el => el.removeEventListener( eventName, this.onNavigatePrevClicked, false ) );
  85. this.controlsNext.forEach( el => el.removeEventListener( eventName, this.onNavigateNextClicked, false ) );
  86. this.controlsFullscreen.forEach( el => el.removeEventListener( eventName, this.onEnterFullscreen, false ) );
  87. } );
  88. }
  89. /**
  90. * Updates the state of all control/navigation arrows.
  91. */
  92. update() {
  93. let routes = this.Reveal.availableRoutes();
  94. // Remove the 'enabled' class from all directions
  95. [...this.controlsLeft, ...this.controlsRight, ...this.controlsUp, ...this.controlsDown, ...this.controlsPrev, ...this.controlsNext].forEach( node => {
  96. node.classList.remove( 'enabled', 'fragmented' );
  97. // Set 'disabled' attribute on all directions
  98. node.setAttribute( 'disabled', 'disabled' );
  99. } );
  100. // Add the 'enabled' class to the available routes; remove 'disabled' attribute to enable buttons
  101. if( routes.left ) this.controlsLeft.forEach( el => { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
  102. if( routes.right ) this.controlsRight.forEach( el => { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
  103. if( routes.up ) this.controlsUp.forEach( el => { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
  104. if( routes.down ) this.controlsDown.forEach( el => { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
  105. // Prev/next buttons
  106. if( routes.left || routes.up ) this.controlsPrev.forEach( el => { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
  107. if( routes.right || routes.down ) this.controlsNext.forEach( el => { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
  108. // Highlight fragment directions
  109. let currentSlide = this.Reveal.getCurrentSlide();
  110. if( currentSlide ) {
  111. let fragmentsRoutes = this.Reveal.fragments.availableRoutes();
  112. // Always apply fragment decorator to prev/next buttons
  113. if( fragmentsRoutes.prev ) this.controlsPrev.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
  114. if( fragmentsRoutes.next ) this.controlsNext.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
  115. const isVerticalStack = this.Reveal.isVerticalSlide( currentSlide );
  116. const hasVerticalSiblings = isVerticalStack &&
  117. currentSlide.parentElement &&
  118. currentSlide.parentElement.querySelectorAll( ':scope > section' ).length > 1;
  119. // Apply fragment decorators to directional buttons based on
  120. // what slide axis they are in
  121. if( isVerticalStack && hasVerticalSiblings ) {
  122. if( fragmentsRoutes.prev ) this.controlsUp.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
  123. if( fragmentsRoutes.next ) this.controlsDown.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
  124. }
  125. else {
  126. if( fragmentsRoutes.prev ) this.controlsLeft.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
  127. if( fragmentsRoutes.next ) this.controlsRight.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
  128. }
  129. }
  130. if( this.Reveal.getConfig().controlsTutorial ) {
  131. let indices = this.Reveal.getIndices();
  132. // Highlight control arrows with an animation to ensure
  133. // that the viewer knows how to navigate
  134. if( !this.Reveal.hasNavigatedVertically() && routes.down ) {
  135. this.controlsDownArrow.classList.add( 'highlight' );
  136. }
  137. else {
  138. this.controlsDownArrow.classList.remove( 'highlight' );
  139. if( this.Reveal.getConfig().rtl ) {
  140. if( !this.Reveal.hasNavigatedHorizontally() && routes.left && indices.v === 0 ) {
  141. this.controlsLeftArrow.classList.add( 'highlight' );
  142. }
  143. else {
  144. this.controlsLeftArrow.classList.remove( 'highlight' );
  145. }
  146. } else {
  147. if( !this.Reveal.hasNavigatedHorizontally() && routes.right && indices.v === 0 ) {
  148. this.controlsRightArrow.classList.add( 'highlight' );
  149. }
  150. else {
  151. this.controlsRightArrow.classList.remove( 'highlight' );
  152. }
  153. }
  154. }
  155. }
  156. }
  157. destroy() {
  158. this.unbind();
  159. this.element.remove();
  160. }
  161. /**
  162. * Event handlers for navigation control buttons.
  163. */
  164. onNavigateLeftClicked( event ) {
  165. event.preventDefault();
  166. this.Reveal.onUserInput();
  167. if( this.Reveal.getConfig().navigationMode === 'linear' ) {
  168. this.Reveal.prev();
  169. }
  170. else {
  171. this.Reveal.left();
  172. }
  173. }
  174. onNavigateRightClicked( event ) {
  175. event.preventDefault();
  176. this.Reveal.onUserInput();
  177. if( this.Reveal.getConfig().navigationMode === 'linear' ) {
  178. this.Reveal.next();
  179. }
  180. else {
  181. this.Reveal.right();
  182. }
  183. }
  184. onNavigateUpClicked( event ) {
  185. event.preventDefault();
  186. this.Reveal.onUserInput();
  187. this.Reveal.up();
  188. }
  189. onNavigateDownClicked( event ) {
  190. event.preventDefault();
  191. this.Reveal.onUserInput();
  192. this.Reveal.down();
  193. }
  194. onNavigatePrevClicked( event ) {
  195. event.preventDefault();
  196. this.Reveal.onUserInput();
  197. this.Reveal.prev();
  198. }
  199. onNavigateNextClicked( event ) {
  200. event.preventDefault();
  201. this.Reveal.onUserInput();
  202. this.Reveal.next();
  203. }
  204. onEnterFullscreen( event ) {
  205. const config = this.Reveal.getConfig();
  206. const viewport = this.Reveal.getViewportElement();
  207. enterFullscreen( config.embedded ? viewport : viewport.parentElement );
  208. }
  209. }