You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

399 lines
10 KiB

  1. import { enterFullscreen } from '../utils/util.js'
  2. /**
  3. * Handles all reveal.js keyboard interactions.
  4. */
  5. export default class Keyboard {
  6. constructor( Reveal ) {
  7. this.Reveal = Reveal;
  8. // A key:value map of keyboard keys and descriptions of
  9. // the actions they trigger
  10. this.shortcuts = {};
  11. // Holds custom key code mappings
  12. this.bindings = {};
  13. this.onDocumentKeyDown = this.onDocumentKeyDown.bind( this );
  14. this.onDocumentKeyPress = this.onDocumentKeyPress.bind( this );
  15. }
  16. /**
  17. * Called when the reveal.js config is updated.
  18. */
  19. configure( config, oldConfig ) {
  20. if( config.navigationMode === 'linear' ) {
  21. this.shortcuts['→ , ↓ , SPACE , N , L , J'] = 'Next slide';
  22. this.shortcuts['← , ↑ , P , H , K'] = 'Previous slide';
  23. }
  24. else {
  25. this.shortcuts['N , SPACE'] = 'Next slide';
  26. this.shortcuts['P , Shift SPACE'] = 'Previous slide';
  27. this.shortcuts['← , H'] = 'Navigate left';
  28. this.shortcuts['→ , L'] = 'Navigate right';
  29. this.shortcuts['↑ , K'] = 'Navigate up';
  30. this.shortcuts['↓ , J'] = 'Navigate down';
  31. }
  32. this.shortcuts['Alt + ←/&#8593/→/↓'] = 'Navigate without fragments';
  33. this.shortcuts['Shift + ←/&#8593/→/↓'] = 'Jump to first/last slide';
  34. this.shortcuts['B , .'] = 'Pause';
  35. this.shortcuts['F'] = 'Fullscreen';
  36. this.shortcuts['G'] = 'Jump to slide';
  37. this.shortcuts['ESC, O'] = 'Slide overview';
  38. }
  39. /**
  40. * Starts listening for keyboard events.
  41. */
  42. bind() {
  43. document.addEventListener( 'keydown', this.onDocumentKeyDown, false );
  44. document.addEventListener( 'keypress', this.onDocumentKeyPress, false );
  45. }
  46. /**
  47. * Stops listening for keyboard events.
  48. */
  49. unbind() {
  50. document.removeEventListener( 'keydown', this.onDocumentKeyDown, false );
  51. document.removeEventListener( 'keypress', this.onDocumentKeyPress, false );
  52. }
  53. /**
  54. * Add a custom key binding with optional description to
  55. * be added to the help screen.
  56. */
  57. addKeyBinding( binding, callback ) {
  58. if( typeof binding === 'object' && binding.keyCode ) {
  59. this.bindings[binding.keyCode] = {
  60. callback: callback,
  61. key: binding.key,
  62. description: binding.description
  63. };
  64. }
  65. else {
  66. this.bindings[binding] = {
  67. callback: callback,
  68. key: null,
  69. description: null
  70. };
  71. }
  72. }
  73. /**
  74. * Removes the specified custom key binding.
  75. */
  76. removeKeyBinding( keyCode ) {
  77. delete this.bindings[keyCode];
  78. }
  79. /**
  80. * Programmatically triggers a keyboard event
  81. *
  82. * @param {int} keyCode
  83. */
  84. triggerKey( keyCode ) {
  85. this.onDocumentKeyDown( { keyCode } );
  86. }
  87. /**
  88. * Registers a new shortcut to include in the help overlay
  89. *
  90. * @param {String} key
  91. * @param {String} value
  92. */
  93. registerKeyboardShortcut( key, value ) {
  94. this.shortcuts[key] = value;
  95. }
  96. getShortcuts() {
  97. return this.shortcuts;
  98. }
  99. getBindings() {
  100. return this.bindings;
  101. }
  102. /**
  103. * Handler for the document level 'keypress' event.
  104. *
  105. * @param {object} event
  106. */
  107. onDocumentKeyPress( event ) {
  108. // Check if the pressed key is question mark
  109. if( event.shiftKey && event.charCode === 63 ) {
  110. this.Reveal.toggleHelp();
  111. }
  112. }
  113. /**
  114. * Handler for the document level 'keydown' event.
  115. *
  116. * @param {object} event
  117. */
  118. onDocumentKeyDown( event ) {
  119. let config = this.Reveal.getConfig();
  120. // If there's a condition specified and it returns false,
  121. // ignore this event
  122. if( typeof config.keyboardCondition === 'function' && config.keyboardCondition(event) === false ) {
  123. return true;
  124. }
  125. // If keyboardCondition is set, only capture keyboard events
  126. // for embedded decks when they are focused
  127. if( config.keyboardCondition === 'focused' && !this.Reveal.isFocused() ) {
  128. return true;
  129. }
  130. // Shorthand
  131. let keyCode = event.keyCode;
  132. // Remember if auto-sliding was paused so we can toggle it
  133. let autoSlideWasPaused = !this.Reveal.isAutoSliding();
  134. this.Reveal.onUserInput( event );
  135. // Is there a focused element that could be using the keyboard?
  136. let activeElementIsCE = document.activeElement && document.activeElement.isContentEditable === true;
  137. let activeElementIsInput = document.activeElement && document.activeElement.tagName && /input|textarea/i.test( document.activeElement.tagName );
  138. let activeElementIsNotes = document.activeElement && document.activeElement.className && /speaker-notes/i.test( document.activeElement.className);
  139. // Whitelist certain modifiers for slide navigation shortcuts
  140. let isNavigationKey = [32, 37, 38, 39, 40, 78, 80].indexOf( event.keyCode ) !== -1;
  141. // Prevent all other events when a modifier is pressed
  142. let unusedModifier = !( isNavigationKey && event.shiftKey || event.altKey ) &&
  143. ( event.shiftKey || event.altKey || event.ctrlKey || event.metaKey );
  144. // Disregard the event if there's a focused element or a
  145. // keyboard modifier key is present
  146. if( activeElementIsCE || activeElementIsInput || activeElementIsNotes || unusedModifier ) return;
  147. // While paused only allow resume keyboard events; 'b', 'v', '.'
  148. let resumeKeyCodes = [66,86,190,191];
  149. let key;
  150. // Custom key bindings for togglePause should be able to resume
  151. if( typeof config.keyboard === 'object' ) {
  152. for( key in config.keyboard ) {
  153. if( config.keyboard[key] === 'togglePause' ) {
  154. resumeKeyCodes.push( parseInt( key, 10 ) );
  155. }
  156. }
  157. }
  158. if( this.Reveal.isPaused() && resumeKeyCodes.indexOf( keyCode ) === -1 ) {
  159. return false;
  160. }
  161. // Use linear navigation if we're configured to OR if
  162. // the presentation is one-dimensional
  163. let useLinearMode = config.navigationMode === 'linear' || !this.Reveal.hasHorizontalSlides() || !this.Reveal.hasVerticalSlides();
  164. let triggered = false;
  165. // 1. User defined key bindings
  166. if( typeof config.keyboard === 'object' ) {
  167. for( key in config.keyboard ) {
  168. // Check if this binding matches the pressed key
  169. if( parseInt( key, 10 ) === keyCode ) {
  170. let value = config.keyboard[ key ];
  171. // Callback function
  172. if( typeof value === 'function' ) {
  173. value.apply( null, [ event ] );
  174. }
  175. // String shortcuts to reveal.js API
  176. else if( typeof value === 'string' && typeof this.Reveal[ value ] === 'function' ) {
  177. this.Reveal[ value ].call();
  178. }
  179. triggered = true;
  180. }
  181. }
  182. }
  183. // 2. Registered custom key bindings
  184. if( triggered === false ) {
  185. for( key in this.bindings ) {
  186. // Check if this binding matches the pressed key
  187. if( parseInt( key, 10 ) === keyCode ) {
  188. let action = this.bindings[ key ].callback;
  189. // Callback function
  190. if( typeof action === 'function' ) {
  191. action.apply( null, [ event ] );
  192. }
  193. // String shortcuts to reveal.js API
  194. else if( typeof action === 'string' && typeof this.Reveal[ action ] === 'function' ) {
  195. this.Reveal[ action ].call();
  196. }
  197. triggered = true;
  198. }
  199. }
  200. }
  201. // 3. System defined key bindings
  202. if( triggered === false ) {
  203. // Assume true and try to prove false
  204. triggered = true;
  205. // P, PAGE UP
  206. if( keyCode === 80 || keyCode === 33 ) {
  207. this.Reveal.prev({skipFragments: event.altKey});
  208. }
  209. // N, PAGE DOWN
  210. else if( keyCode === 78 || keyCode === 34 ) {
  211. this.Reveal.next({skipFragments: event.altKey});
  212. }
  213. // H, LEFT
  214. else if( keyCode === 72 || keyCode === 37 ) {
  215. if( event.shiftKey ) {
  216. this.Reveal.slide( 0 );
  217. }
  218. else if( !this.Reveal.overview.isActive() && useLinearMode ) {
  219. this.Reveal.prev({skipFragments: event.altKey});
  220. }
  221. else {
  222. this.Reveal.left({skipFragments: event.altKey});
  223. }
  224. }
  225. // L, RIGHT
  226. else if( keyCode === 76 || keyCode === 39 ) {
  227. if( event.shiftKey ) {
  228. this.Reveal.slide( this.Reveal.getHorizontalSlides().length - 1 );
  229. }
  230. else if( !this.Reveal.overview.isActive() && useLinearMode ) {
  231. this.Reveal.next({skipFragments: event.altKey});
  232. }
  233. else {
  234. this.Reveal.right({skipFragments: event.altKey});
  235. }
  236. }
  237. // K, UP
  238. else if( keyCode === 75 || keyCode === 38 ) {
  239. if( event.shiftKey ) {
  240. this.Reveal.slide( undefined, 0 );
  241. }
  242. else if( !this.Reveal.overview.isActive() && useLinearMode ) {
  243. this.Reveal.prev({skipFragments: event.altKey});
  244. }
  245. else {
  246. this.Reveal.up({skipFragments: event.altKey});
  247. }
  248. }
  249. // J, DOWN
  250. else if( keyCode === 74 || keyCode === 40 ) {
  251. if( event.shiftKey ) {
  252. this.Reveal.slide( undefined, Number.MAX_VALUE );
  253. }
  254. else if( !this.Reveal.overview.isActive() && useLinearMode ) {
  255. this.Reveal.next({skipFragments: event.altKey});
  256. }
  257. else {
  258. this.Reveal.down({skipFragments: event.altKey});
  259. }
  260. }
  261. // HOME
  262. else if( keyCode === 36 ) {
  263. this.Reveal.slide( 0 );
  264. }
  265. // END
  266. else if( keyCode === 35 ) {
  267. this.Reveal.slide( this.Reveal.getHorizontalSlides().length - 1 );
  268. }
  269. // SPACE
  270. else if( keyCode === 32 ) {
  271. if( this.Reveal.overview.isActive() ) {
  272. this.Reveal.overview.deactivate();
  273. }
  274. if( event.shiftKey ) {
  275. this.Reveal.prev({skipFragments: event.altKey});
  276. }
  277. else {
  278. this.Reveal.next({skipFragments: event.altKey});
  279. }
  280. }
  281. // TWO-SPOT, SEMICOLON, B, V, PERIOD, LOGITECH PRESENTER TOOLS "BLACK SCREEN" BUTTON
  282. else if( keyCode === 58 || keyCode === 59 || keyCode === 66 || keyCode === 86 || keyCode === 190 || keyCode === 191 ) {
  283. this.Reveal.togglePause();
  284. }
  285. // F
  286. else if( keyCode === 70 ) {
  287. enterFullscreen( config.embedded ? this.Reveal.getViewportElement() : document.documentElement );
  288. }
  289. // A
  290. else if( keyCode === 65 ) {
  291. if ( config.autoSlideStoppable ) {
  292. this.Reveal.toggleAutoSlide( autoSlideWasPaused );
  293. }
  294. }
  295. // G
  296. else if( keyCode === 71 ) {
  297. if ( config.jumpToSlide ) {
  298. this.Reveal.toggleJumpToSlide();
  299. }
  300. }
  301. else {
  302. triggered = false;
  303. }
  304. }
  305. // If the input resulted in a triggered action we should prevent
  306. // the browsers default behavior
  307. if( triggered ) {
  308. event.preventDefault && event.preventDefault();
  309. }
  310. // ESC or O key
  311. else if( keyCode === 27 || keyCode === 79 ) {
  312. if( this.Reveal.closeOverlay() === false ) {
  313. this.Reveal.overview.toggle();
  314. }
  315. event.preventDefault && event.preventDefault();
  316. }
  317. // If auto-sliding is enabled we need to cue up
  318. // another timeout
  319. this.Reveal.cueAutoSlide();
  320. }
  321. }