diff options
Diffstat (limited to 'present/js/controllers/keyboard.js')
| -rw-r--r-- | present/js/controllers/keyboard.js | 399 | 
1 files changed, 399 insertions, 0 deletions
| diff --git a/present/js/controllers/keyboard.js b/present/js/controllers/keyboard.js new file mode 100644 index 0000000..e3bff7a --- /dev/null +++ b/present/js/controllers/keyboard.js @@ -0,0 +1,399 @@ +import { enterFullscreen } from '../utils/util.js' + +/** + * Handles all reveal.js keyboard interactions. + */ +export default class Keyboard { + +	constructor( Reveal ) { + +		this.Reveal = Reveal; + +		// A key:value map of keyboard keys and descriptions of +		// the actions they trigger +		this.shortcuts = {}; + +		// Holds custom key code mappings +		this.bindings = {}; + +		this.onDocumentKeyDown = this.onDocumentKeyDown.bind( this ); +		this.onDocumentKeyPress = this.onDocumentKeyPress.bind( this ); + +	} + +	/** +	 * Called when the reveal.js config is updated. +	 */ +	configure( config, oldConfig ) { + +		if( config.navigationMode === 'linear' ) { +			this.shortcuts['→  ,  ↓  ,  SPACE  ,  N  ,  L  ,  J'] = 'Next slide'; +			this.shortcuts['←  ,  ↑  ,  P  ,  H  ,  K']           = 'Previous slide'; +		} +		else { +			this.shortcuts['N  ,  SPACE']   = 'Next slide'; +			this.shortcuts['P  ,  Shift SPACE']             = 'Previous slide'; +			this.shortcuts['←  ,  H'] = 'Navigate left'; +			this.shortcuts['→  ,  L'] = 'Navigate right'; +			this.shortcuts['↑  ,  K'] = 'Navigate up'; +			this.shortcuts['↓  ,  J'] = 'Navigate down'; +		} + +		this.shortcuts['Alt + ←/↑/→/↓']        = 'Navigate without fragments'; +		this.shortcuts['Shift + ←/↑/→/↓']      = 'Jump to first/last slide'; +		this.shortcuts['B  ,  .']                       = 'Pause'; +		this.shortcuts['F']                             = 'Fullscreen'; +		this.shortcuts['G']                             = 'Jump to slide'; +		this.shortcuts['ESC, O']                        = 'Slide overview'; + +	} + +	/** +	 * Starts listening for keyboard events. +	 */ +	bind() { + +		document.addEventListener( 'keydown', this.onDocumentKeyDown, false ); +		document.addEventListener( 'keypress', this.onDocumentKeyPress, false ); + +	} + +	/** +	 * Stops listening for keyboard events. +	 */ +	unbind() { + +		document.removeEventListener( 'keydown', this.onDocumentKeyDown, false ); +		document.removeEventListener( 'keypress', this.onDocumentKeyPress, false ); + +	} + +	/** +	 * Add a custom key binding with optional description to +	 * be added to the help screen. +	 */ +	addKeyBinding( binding, callback ) { + +		if( typeof binding === 'object' && binding.keyCode ) { +			this.bindings[binding.keyCode] = { +				callback: callback, +				key: binding.key, +				description: binding.description +			}; +		} +		else { +			this.bindings[binding] = { +				callback: callback, +				key: null, +				description: null +			}; +		} + +	} + +	/** +	 * Removes the specified custom key binding. +	 */ +	removeKeyBinding( keyCode ) { + +		delete this.bindings[keyCode]; + +	} + +	/** +	 * Programmatically triggers a keyboard event +	 * +	 * @param {int} keyCode +	 */ +	triggerKey( keyCode ) { + +		this.onDocumentKeyDown( { keyCode } ); + +	} + +	/** +	 * Registers a new shortcut to include in the help overlay +	 * +	 * @param {String} key +	 * @param {String} value +	 */ +	registerKeyboardShortcut( key, value ) { + +		this.shortcuts[key] = value; + +	} + +	getShortcuts() { + +		return this.shortcuts; + +	} + +	getBindings() { + +		return this.bindings; + +	} + +	/** +	 * Handler for the document level 'keypress' event. +	 * +	 * @param {object} event +	 */ +	onDocumentKeyPress( event ) { + +		// Check if the pressed key is question mark +		if( event.shiftKey && event.charCode === 63 ) { +			this.Reveal.toggleHelp(); +		} + +	} + +	/** +	 * Handler for the document level 'keydown' event. +	 * +	 * @param {object} event +	 */ +	onDocumentKeyDown( event ) { + +		let config = this.Reveal.getConfig(); + +		// If there's a condition specified and it returns false, +		// ignore this event +		if( typeof config.keyboardCondition === 'function' && config.keyboardCondition(event) === false ) { +			return true; +		} + +		// If keyboardCondition is set, only capture keyboard events +		// for embedded decks when they are focused +		if( config.keyboardCondition === 'focused' && !this.Reveal.isFocused() ) { +			return true; +		} + +		// Shorthand +		let keyCode = event.keyCode; + +		// Remember if auto-sliding was paused so we can toggle it +		let autoSlideWasPaused = !this.Reveal.isAutoSliding(); + +		this.Reveal.onUserInput( event ); + +		// Is there a focused element that could be using the keyboard? +		let activeElementIsCE = document.activeElement && document.activeElement.isContentEditable === true; +		let activeElementIsInput = document.activeElement && document.activeElement.tagName && /input|textarea/i.test( document.activeElement.tagName ); +		let activeElementIsNotes = document.activeElement && document.activeElement.className && /speaker-notes/i.test( document.activeElement.className); + +		// Whitelist certain modifiers for slide navigation shortcuts +		let isNavigationKey = [32, 37, 38, 39, 40, 78, 80].indexOf( event.keyCode ) !== -1; + +		// Prevent all other events when a modifier is pressed +		let unusedModifier = 	!( isNavigationKey && event.shiftKey || event.altKey ) && +								( event.shiftKey || event.altKey || event.ctrlKey || event.metaKey ); + +		// Disregard the event if there's a focused element or a +		// keyboard modifier key is present +		if( activeElementIsCE || activeElementIsInput || activeElementIsNotes || unusedModifier ) return; + +		// While paused only allow resume keyboard events; 'b', 'v', '.' +		let resumeKeyCodes = [66,86,190,191]; +		let key; + +		// Custom key bindings for togglePause should be able to resume +		if( typeof config.keyboard === 'object' ) { +			for( key in config.keyboard ) { +				if( config.keyboard[key] === 'togglePause' ) { +					resumeKeyCodes.push( parseInt( key, 10 ) ); +				} +			} +		} + +		if( this.Reveal.isPaused() && resumeKeyCodes.indexOf( keyCode ) === -1 ) { +			return false; +		} + +		// Use linear navigation if we're configured to OR if +		// the presentation is one-dimensional +		let useLinearMode = config.navigationMode === 'linear' || !this.Reveal.hasHorizontalSlides() || !this.Reveal.hasVerticalSlides(); + +		let triggered = false; + +		// 1. User defined key bindings +		if( typeof config.keyboard === 'object' ) { + +			for( key in config.keyboard ) { + +				// Check if this binding matches the pressed key +				if( parseInt( key, 10 ) === keyCode ) { + +					let value = config.keyboard[ key ]; + +					// Callback function +					if( typeof value === 'function' ) { +						value.apply( null, [ event ] ); +					} +					// String shortcuts to reveal.js API +					else if( typeof value === 'string' && typeof this.Reveal[ value ] === 'function' ) { +						this.Reveal[ value ].call(); +					} + +					triggered = true; + +				} + +			} + +		} + +		// 2. Registered custom key bindings +		if( triggered === false ) { + +			for( key in this.bindings ) { + +				// Check if this binding matches the pressed key +				if( parseInt( key, 10 ) === keyCode ) { + +					let action = this.bindings[ key ].callback; + +					// Callback function +					if( typeof action === 'function' ) { +						action.apply( null, [ event ] ); +					} +					// String shortcuts to reveal.js API +					else if( typeof action === 'string' && typeof this.Reveal[ action ] === 'function' ) { +						this.Reveal[ action ].call(); +					} + +					triggered = true; +				} +			} +		} + +		// 3. System defined key bindings +		if( triggered === false ) { + +			// Assume true and try to prove false +			triggered = true; + +			// P, PAGE UP +			if( keyCode === 80 || keyCode === 33 ) { +				this.Reveal.prev({skipFragments: event.altKey}); +			} +			// N, PAGE DOWN +			else if( keyCode === 78 || keyCode === 34 ) { +				this.Reveal.next({skipFragments: event.altKey}); +			} +			// H, LEFT +			else if( keyCode === 72 || keyCode === 37 ) { +				if( event.shiftKey ) { +					this.Reveal.slide( 0 ); +				} +				else if( !this.Reveal.overview.isActive() && useLinearMode ) { +					this.Reveal.prev({skipFragments: event.altKey}); +				} +				else { +					this.Reveal.left({skipFragments: event.altKey}); +				} +			} +			// L, RIGHT +			else if( keyCode === 76 || keyCode === 39 ) { +				if( event.shiftKey ) { +					this.Reveal.slide( this.Reveal.getHorizontalSlides().length - 1 ); +				} +				else if( !this.Reveal.overview.isActive() && useLinearMode ) { +					this.Reveal.next({skipFragments: event.altKey}); +				} +				else { +					this.Reveal.right({skipFragments: event.altKey}); +				} +			} +			// K, UP +			else if( keyCode === 75 || keyCode === 38 ) { +				if( event.shiftKey ) { +					this.Reveal.slide( undefined, 0 ); +				} +				else if( !this.Reveal.overview.isActive() && useLinearMode ) { +					this.Reveal.prev({skipFragments: event.altKey}); +				} +				else { +					this.Reveal.up({skipFragments: event.altKey}); +				} +			} +			// J, DOWN +			else if( keyCode === 74 || keyCode === 40 ) { +				if( event.shiftKey ) { +					this.Reveal.slide( undefined, Number.MAX_VALUE ); +				} +				else if( !this.Reveal.overview.isActive() && useLinearMode ) { +					this.Reveal.next({skipFragments: event.altKey}); +				} +				else { +					this.Reveal.down({skipFragments: event.altKey}); +				} +			} +			// HOME +			else if( keyCode === 36 ) { +				this.Reveal.slide( 0 ); +			} +			// END +			else if( keyCode === 35 ) { +				this.Reveal.slide( this.Reveal.getHorizontalSlides().length - 1 ); +			} +			// SPACE +			else if( keyCode === 32 ) { +				if( this.Reveal.overview.isActive() ) { +					this.Reveal.overview.deactivate(); +				} +				if( event.shiftKey ) { +					this.Reveal.prev({skipFragments: event.altKey}); +				} +				else { +					this.Reveal.next({skipFragments: event.altKey}); +				} +			} +			// TWO-SPOT, SEMICOLON, B, V, PERIOD, LOGITECH PRESENTER TOOLS "BLACK SCREEN" BUTTON +			else if( keyCode === 58 || keyCode === 59 || keyCode === 66 || keyCode === 86 || keyCode === 190 || keyCode === 191 ) { +				this.Reveal.togglePause(); +			} +			// F +			else if( keyCode === 70 ) { +				enterFullscreen( config.embedded ? this.Reveal.getViewportElement() : document.documentElement ); +			} +			// A +			else if( keyCode === 65 ) { +				if ( config.autoSlideStoppable ) { +					this.Reveal.toggleAutoSlide( autoSlideWasPaused ); +				} +			} +			// G +			else if( keyCode === 71 ) { +				if ( config.jumpToSlide ) { +					this.Reveal.toggleJumpToSlide(); +				} +			} +			else { +				triggered = false; +			} + +		} + +		// If the input resulted in a triggered action we should prevent +		// the browsers default behavior +		if( triggered ) { +			event.preventDefault && event.preventDefault(); +		} +		// ESC or O key +		else if( keyCode === 27 || keyCode === 79 ) { +			if( this.Reveal.closeOverlay() === false ) { +				this.Reveal.overview.toggle(); +			} + +			event.preventDefault && event.preventDefault(); +		} + +		// If auto-sliding is enabled we need to cue up +		// another timeout +		this.Reveal.cueAutoSlide(); + +	} + +}
\ No newline at end of file | 
