1
0

controls.js 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  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. // Apply fragment decorators to directional buttons based on
  116. // what slide axis they are in
  117. if( this.Reveal.isVerticalSlide( currentSlide ) ) {
  118. if( fragmentsRoutes.prev ) this.controlsUp.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
  119. if( fragmentsRoutes.next ) this.controlsDown.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
  120. }
  121. else {
  122. if( fragmentsRoutes.prev ) this.controlsLeft.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
  123. if( fragmentsRoutes.next ) this.controlsRight.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
  124. }
  125. }
  126. if( this.Reveal.getConfig().controlsTutorial ) {
  127. let indices = this.Reveal.getIndices();
  128. // Highlight control arrows with an animation to ensure
  129. // that the viewer knows how to navigate
  130. if( !this.Reveal.hasNavigatedVertically() && routes.down ) {
  131. this.controlsDownArrow.classList.add( 'highlight' );
  132. }
  133. else {
  134. this.controlsDownArrow.classList.remove( 'highlight' );
  135. if( this.Reveal.getConfig().rtl ) {
  136. if( !this.Reveal.hasNavigatedHorizontally() && routes.left && indices.v === 0 ) {
  137. this.controlsLeftArrow.classList.add( 'highlight' );
  138. }
  139. else {
  140. this.controlsLeftArrow.classList.remove( 'highlight' );
  141. }
  142. } else {
  143. if( !this.Reveal.hasNavigatedHorizontally() && routes.right && indices.v === 0 ) {
  144. this.controlsRightArrow.classList.add( 'highlight' );
  145. }
  146. else {
  147. this.controlsRightArrow.classList.remove( 'highlight' );
  148. }
  149. }
  150. }
  151. }
  152. }
  153. destroy() {
  154. this.unbind();
  155. this.element.remove();
  156. }
  157. /**
  158. * Event handlers for navigation control buttons.
  159. */
  160. onNavigateLeftClicked( event ) {
  161. event.preventDefault();
  162. this.Reveal.onUserInput();
  163. if( this.Reveal.getConfig().navigationMode === 'linear' ) {
  164. this.Reveal.prev();
  165. }
  166. else {
  167. this.Reveal.left();
  168. }
  169. }
  170. onNavigateRightClicked( event ) {
  171. event.preventDefault();
  172. this.Reveal.onUserInput();
  173. if( this.Reveal.getConfig().navigationMode === 'linear' ) {
  174. this.Reveal.next();
  175. }
  176. else {
  177. this.Reveal.right();
  178. }
  179. }
  180. onNavigateUpClicked( event ) {
  181. event.preventDefault();
  182. this.Reveal.onUserInput();
  183. this.Reveal.up();
  184. }
  185. onNavigateDownClicked( event ) {
  186. event.preventDefault();
  187. this.Reveal.onUserInput();
  188. this.Reveal.down();
  189. }
  190. onNavigatePrevClicked( event ) {
  191. event.preventDefault();
  192. this.Reveal.onUserInput();
  193. this.Reveal.prev();
  194. }
  195. onNavigateNextClicked( event ) {
  196. event.preventDefault();
  197. this.Reveal.onUserInput();
  198. this.Reveal.next();
  199. }
  200. onEnterFullscreen( event ) {
  201. const config = this.Reveal.getConfig();
  202. const viewport = this.Reveal.getViewportElement();
  203. enterFullscreen( config.embedded ? viewport : viewport.parentElement );
  204. }
  205. }