keyboard.js 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  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'] = '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['Home , Shift ←'] = 'First slide';
  33. this.shortcuts['End , Shift →'] = '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. /**
  96. * Handler for the document level 'keypress' event.
  97. *
  98. * @param {object} event
  99. */
  100. onDocumentKeyPress( event ) {
  101. // Check if the pressed key is question mark
  102. if( event.shiftKey && event.charCode === 63 ) {
  103. this.Reveal.toggleHelp();
  104. }
  105. }
  106. /**
  107. * Handler for the document level 'keydown' event.
  108. *
  109. * @param {object} event
  110. */
  111. onDocumentKeyDown( event ) {
  112. let config = this.Reveal.getConfig();
  113. // If there's a condition specified and it returns false,
  114. // ignore this event
  115. if( typeof config.keyboardCondition === 'function' && config.keyboardCondition(event) === false ) {
  116. return true;
  117. }
  118. // Shorthand
  119. let keyCode = event.keyCode;
  120. // Remember if auto-sliding was paused so we can toggle it
  121. let autoSlideWasPaused = !this.Reveal.isAutoSliding();
  122. this.Reveal.onUserInput( event );
  123. // Is there a focused element that could be using the keyboard?
  124. let activeElementIsCE = document.activeElement && document.activeElement.contentEditable !== 'inherit';
  125. let activeElementIsInput = document.activeElement && document.activeElement.tagName && /input|textarea/i.test( document.activeElement.tagName );
  126. let activeElementIsNotes = document.activeElement && document.activeElement.className && /speaker-notes/i.test( document.activeElement.className);
  127. // Whitelist specific modified + keycode combinations
  128. let prevSlideShortcut = event.shiftKey && event.keyCode === 32;
  129. let firstSlideShortcut = event.shiftKey && keyCode === 37;
  130. let lastSlideShortcut = event.shiftKey && keyCode === 39;
  131. // Prevent all other events when a modifier is pressed
  132. let unusedModifier = !prevSlideShortcut && !firstSlideShortcut && !lastSlideShortcut &&
  133. ( event.shiftKey || event.altKey || event.ctrlKey || event.metaKey );
  134. // Disregard the event if there's a focused element or a
  135. // keyboard modifier key is present
  136. if( activeElementIsCE || activeElementIsInput || activeElementIsNotes || unusedModifier ) return;
  137. // While paused only allow resume keyboard events; 'b', 'v', '.'
  138. let resumeKeyCodes = [66,86,190,191];
  139. let key;
  140. // Custom key bindings for togglePause should be able to resume
  141. if( typeof config.keyboard === 'object' ) {
  142. for( key in config.keyboard ) {
  143. if( config.keyboard[key] === 'togglePause' ) {
  144. resumeKeyCodes.push( parseInt( key, 10 ) );
  145. }
  146. }
  147. }
  148. if( this.Reveal.isPaused() && resumeKeyCodes.indexOf( keyCode ) === -1 ) {
  149. return false;
  150. }
  151. // Use linear navigation if we're configured to OR if
  152. // the presentation is one-dimensional
  153. let useLinearMode = config.navigationMode === 'linear' || !this.Reveal.hasHorizontalSlides() || !this.Reveal.hasVerticalSlides();
  154. let triggered = false;
  155. // 1. User defined key bindings
  156. if( typeof config.keyboard === 'object' ) {
  157. for( key in config.keyboard ) {
  158. // Check if this binding matches the pressed key
  159. if( parseInt( key, 10 ) === keyCode ) {
  160. let value = config.keyboard[ key ];
  161. // Callback function
  162. if( typeof value === 'function' ) {
  163. value.apply( null, [ event ] );
  164. }
  165. // String shortcuts to reveal.js API
  166. else if( typeof value === 'string' && typeof this.Reveal[ value ] === 'function' ) {
  167. this.Reveal[ value ].call();
  168. }
  169. triggered = true;
  170. }
  171. }
  172. }
  173. // 2. Registered custom key bindings
  174. if( triggered === false ) {
  175. for( key in this.bindings ) {
  176. // Check if this binding matches the pressed key
  177. if( parseInt( key, 10 ) === keyCode ) {
  178. let action = this.bindings[ key ].callback;
  179. // Callback function
  180. if( typeof action === 'function' ) {
  181. action.apply( null, [ event ] );
  182. }
  183. // String shortcuts to reveal.js API
  184. else if( typeof action === 'string' && typeof this.Reveal[ action ] === 'function' ) {
  185. this.Reveal[ action ].call();
  186. }
  187. triggered = true;
  188. }
  189. }
  190. }
  191. // 3. System defined key bindings
  192. if( triggered === false ) {
  193. // Assume true and try to prove false
  194. triggered = true;
  195. // P, PAGE UP
  196. if( keyCode === 80 || keyCode === 33 ) {
  197. this.Reveal.prev();
  198. }
  199. // N, PAGE DOWN
  200. else if( keyCode === 78 || keyCode === 34 ) {
  201. this.Reveal.next();
  202. }
  203. // H, LEFT
  204. else if( keyCode === 72 || keyCode === 37 ) {
  205. if( firstSlideShortcut ) {
  206. this.Reveal.slide( 0 );
  207. }
  208. else if( !this.Reveal.overview.isActive() && useLinearMode ) {
  209. this.Reveal.prev();
  210. }
  211. else {
  212. this.Reveal.left();
  213. }
  214. }
  215. // L, RIGHT
  216. else if( keyCode === 76 || keyCode === 39 ) {
  217. if( lastSlideShortcut ) {
  218. this.Reveal.slide( Number.MAX_VALUE );
  219. }
  220. else if( !this.Reveal.overview.isActive() && useLinearMode ) {
  221. this.Reveal.next();
  222. }
  223. else {
  224. this.Reveal.right();
  225. }
  226. }
  227. // K, UP
  228. else if( keyCode === 75 || keyCode === 38 ) {
  229. if( !this.Reveal.overview.isActive() && useLinearMode ) {
  230. this.Reveal.prev();
  231. }
  232. else {
  233. this.Reveal.up();
  234. }
  235. }
  236. // J, DOWN
  237. else if( keyCode === 74 || keyCode === 40 ) {
  238. if( !this.Reveal.overview.isActive() && useLinearMode ) {
  239. this.Reveal.next();
  240. }
  241. else {
  242. this.Reveal.down();
  243. }
  244. }
  245. // HOME
  246. else if( keyCode === 36 ) {
  247. this.Reveal.slide( 0 );
  248. }
  249. // END
  250. else if( keyCode === 35 ) {
  251. this.Reveal.slide( Number.MAX_VALUE );
  252. }
  253. // SPACE
  254. else if( keyCode === 32 ) {
  255. if( this.Reveal.overview.isActive() ) {
  256. this.Reveal.overview.deactivate();
  257. }
  258. if( event.shiftKey ) {
  259. this.Reveal.prev();
  260. }
  261. else {
  262. this.Reveal.next();
  263. }
  264. }
  265. // TWO-SPOT, SEMICOLON, B, V, PERIOD, LOGITECH PRESENTER TOOLS "BLACK SCREEN" BUTTON
  266. else if( keyCode === 58 || keyCode === 59 || keyCode === 66 || keyCode === 86 || keyCode === 190 || keyCode === 191 ) {
  267. this.Reveal.togglePause();
  268. }
  269. // F
  270. else if( keyCode === 70 ) {
  271. enterFullscreen();
  272. }
  273. // A
  274. else if( keyCode === 65 ) {
  275. if ( config.autoSlideStoppable ) {
  276. this.Reveal.toggleAutoSlide( autoSlideWasPaused );
  277. }
  278. }
  279. else {
  280. triggered = false;
  281. }
  282. }
  283. // If the input resulted in a triggered action we should prevent
  284. // the browsers default behavior
  285. if( triggered ) {
  286. event.preventDefault && event.preventDefault();
  287. }
  288. // ESC or O key
  289. else if( keyCode === 27 || keyCode === 79 ) {
  290. if( this.Reveal.closeOverlay() === false ) {
  291. this.Reveal.overview.toggle();
  292. }
  293. event.preventDefault && event.preventDefault();
  294. }
  295. // If auto-sliding is enabled we need to cue up
  296. // another timeout
  297. this.Reveal.cueAutoSlide();
  298. }
  299. }