keyboard.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  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['ESC, O'] = 'Slide overview';
  37. }
  38. /**
  39. * Starts listening for keyboard events.
  40. */
  41. bind() {
  42. document.addEventListener( 'keydown', this.onDocumentKeyDown, false );
  43. document.addEventListener( 'keypress', this.onDocumentKeyPress, false );
  44. }
  45. /**
  46. * Stops listening for keyboard events.
  47. */
  48. unbind() {
  49. document.removeEventListener( 'keydown', this.onDocumentKeyDown, false );
  50. document.removeEventListener( 'keypress', this.onDocumentKeyPress, false );
  51. }
  52. /**
  53. * Add a custom key binding with optional description to
  54. * be added to the help screen.
  55. */
  56. addKeyBinding( binding, callback ) {
  57. if( typeof binding === 'object' && binding.keyCode ) {
  58. this.bindings[binding.keyCode] = {
  59. callback: callback,
  60. key: binding.key,
  61. description: binding.description
  62. };
  63. }
  64. else {
  65. this.bindings[binding] = {
  66. callback: callback,
  67. key: null,
  68. description: null
  69. };
  70. }
  71. }
  72. /**
  73. * Removes the specified custom key binding.
  74. */
  75. removeKeyBinding( keyCode ) {
  76. delete this.bindings[keyCode];
  77. }
  78. /**
  79. * Programmatically triggers a keyboard event
  80. *
  81. * @param {int} keyCode
  82. */
  83. triggerKey( keyCode ) {
  84. this.onDocumentKeyDown( { keyCode } );
  85. }
  86. /**
  87. * Registers a new shortcut to include in the help overlay
  88. *
  89. * @param {String} key
  90. * @param {String} value
  91. */
  92. registerKeyboardShortcut( key, value ) {
  93. this.shortcuts[key] = value;
  94. }
  95. getShortcuts() {
  96. return this.shortcuts;
  97. }
  98. getBindings() {
  99. return this.bindings;
  100. }
  101. /**
  102. * Handler for the document level 'keypress' event.
  103. *
  104. * @param {object} event
  105. */
  106. onDocumentKeyPress( event ) {
  107. // Check if the pressed key is question mark
  108. if( event.shiftKey && event.charCode === 63 ) {
  109. this.Reveal.toggleHelp();
  110. }
  111. }
  112. /**
  113. * Handler for the document level 'keydown' event.
  114. *
  115. * @param {object} event
  116. */
  117. onDocumentKeyDown( event ) {
  118. let config = this.Reveal.getConfig();
  119. // If there's a condition specified and it returns false,
  120. // ignore this event
  121. if( typeof config.keyboardCondition === 'function' && config.keyboardCondition(event) === false ) {
  122. return true;
  123. }
  124. // If keyboardCondition is set, only capture keyboard events
  125. // for embedded decks when they are focused
  126. if( config.keyboardCondition === 'focused' && !this.Reveal.isFocused() ) {
  127. return true;
  128. }
  129. // Shorthand
  130. let keyCode = event.keyCode;
  131. // Remember if auto-sliding was paused so we can toggle it
  132. let autoSlideWasPaused = !this.Reveal.isAutoSliding();
  133. this.Reveal.onUserInput( event );
  134. // Is there a focused element that could be using the keyboard?
  135. let activeElementIsCE = document.activeElement && document.activeElement.isContentEditable === true;
  136. let activeElementIsInput = document.activeElement && document.activeElement.tagName && /input|textarea/i.test( document.activeElement.tagName );
  137. let activeElementIsNotes = document.activeElement && document.activeElement.className && /speaker-notes/i.test( document.activeElement.className);
  138. // Whitelist certain modifiers for slide navigation shortcuts
  139. let isNavigationKey = [32, 37, 38, 39, 40, 78, 80].indexOf( event.keyCode ) !== -1;
  140. // Prevent all other events when a modifier is pressed
  141. let unusedModifier = !( isNavigationKey && event.shiftKey || event.altKey ) &&
  142. ( event.shiftKey || event.altKey || event.ctrlKey || event.metaKey );
  143. // Disregard the event if there's a focused element or a
  144. // keyboard modifier key is present
  145. if( activeElementIsCE || activeElementIsInput || activeElementIsNotes || unusedModifier ) return;
  146. // While paused only allow resume keyboard events; 'b', 'v', '.'
  147. let resumeKeyCodes = [66,86,190,191];
  148. let key;
  149. // Custom key bindings for togglePause should be able to resume
  150. if( typeof config.keyboard === 'object' ) {
  151. for( key in config.keyboard ) {
  152. if( config.keyboard[key] === 'togglePause' ) {
  153. resumeKeyCodes.push( parseInt( key, 10 ) );
  154. }
  155. }
  156. }
  157. if( this.Reveal.isPaused() && resumeKeyCodes.indexOf( keyCode ) === -1 ) {
  158. return false;
  159. }
  160. // Use linear navigation if we're configured to OR if
  161. // the presentation is one-dimensional
  162. let useLinearMode = config.navigationMode === 'linear' || !this.Reveal.hasHorizontalSlides() || !this.Reveal.hasVerticalSlides();
  163. let triggered = false;
  164. // 1. User defined key bindings
  165. if( typeof config.keyboard === 'object' ) {
  166. for( key in config.keyboard ) {
  167. // Check if this binding matches the pressed key
  168. if( parseInt( key, 10 ) === keyCode ) {
  169. let value = config.keyboard[ key ];
  170. // Callback function
  171. if( typeof value === 'function' ) {
  172. value.apply( null, [ event ] );
  173. }
  174. // String shortcuts to reveal.js API
  175. else if( typeof value === 'string' && typeof this.Reveal[ value ] === 'function' ) {
  176. this.Reveal[ value ].call();
  177. }
  178. triggered = true;
  179. }
  180. }
  181. }
  182. // 2. Registered custom key bindings
  183. if( triggered === false ) {
  184. for( key in this.bindings ) {
  185. // Check if this binding matches the pressed key
  186. if( parseInt( key, 10 ) === keyCode ) {
  187. let action = this.bindings[ key ].callback;
  188. // Callback function
  189. if( typeof action === 'function' ) {
  190. action.apply( null, [ event ] );
  191. }
  192. // String shortcuts to reveal.js API
  193. else if( typeof action === 'string' && typeof this.Reveal[ action ] === 'function' ) {
  194. this.Reveal[ action ].call();
  195. }
  196. triggered = true;
  197. }
  198. }
  199. }
  200. // 3. System defined key bindings
  201. if( triggered === false ) {
  202. // Assume true and try to prove false
  203. triggered = true;
  204. // P, PAGE UP
  205. if( keyCode === 80 || keyCode === 33 ) {
  206. this.Reveal.prev({skipFragments: event.altKey});
  207. }
  208. // N, PAGE DOWN
  209. else if( keyCode === 78 || keyCode === 34 ) {
  210. this.Reveal.next({skipFragments: event.altKey});
  211. }
  212. // H, LEFT
  213. else if( keyCode === 72 || keyCode === 37 ) {
  214. if( event.shiftKey ) {
  215. this.Reveal.slide( 0 );
  216. }
  217. else if( !this.Reveal.overview.isActive() && useLinearMode ) {
  218. this.Reveal.prev({skipFragments: event.altKey});
  219. }
  220. else {
  221. this.Reveal.left({skipFragments: event.altKey});
  222. }
  223. }
  224. // L, RIGHT
  225. else if( keyCode === 76 || keyCode === 39 ) {
  226. if( event.shiftKey ) {
  227. this.Reveal.slide( this.Reveal.getHorizontalSlides().length - 1 );
  228. }
  229. else if( !this.Reveal.overview.isActive() && useLinearMode ) {
  230. this.Reveal.next({skipFragments: event.altKey});
  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( keyCode === 58 || keyCode === 59 || keyCode === 66 || keyCode === 86 || keyCode === 190 || keyCode === 191 ) {
  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. else {
  295. triggered = false;
  296. }
  297. }
  298. // If the input resulted in a triggered action we should prevent
  299. // the browsers default behavior
  300. if( triggered ) {
  301. event.preventDefault && event.preventDefault();
  302. }
  303. // ESC or O key
  304. else if( keyCode === 27 || keyCode === 79 ) {
  305. if( this.Reveal.closeOverlay() === false ) {
  306. this.Reveal.overview.toggle();
  307. }
  308. event.preventDefault && event.preventDefault();
  309. }
  310. // If auto-sliding is enabled we need to cue up
  311. // another timeout
  312. this.Reveal.cueAutoSlide();
  313. }
  314. }