1
0

keyboard.js 9.5 KB

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