keyboard.js 9.6 KB

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