keyboard.js 10 KB

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