diff options
Diffstat (limited to 'present/js/controllers/controls.js')
| -rw-r--r-- | present/js/controllers/controls.js | 266 | 
1 files changed, 266 insertions, 0 deletions
| diff --git a/present/js/controllers/controls.js b/present/js/controllers/controls.js new file mode 100644 index 0000000..734eb17 --- /dev/null +++ b/present/js/controllers/controls.js @@ -0,0 +1,266 @@ +import { queryAll } from '../utils/util.js' +import { isAndroid } from '../utils/device.js' + +/** + * Manages our presentation controls. This includes both + * the built-in control arrows as well as event monitoring + * of any elements within the presentation with either of the + * following helper classes: + * - .navigate-up + * - .navigate-right + * - .navigate-down + * - .navigate-left + * - .navigate-next + * - .navigate-prev + */ +export default class Controls { + +	constructor( Reveal ) { + +		this.Reveal = Reveal; + +		this.onNavigateLeftClicked = this.onNavigateLeftClicked.bind( this ); +		this.onNavigateRightClicked = this.onNavigateRightClicked.bind( this ); +		this.onNavigateUpClicked = this.onNavigateUpClicked.bind( this ); +		this.onNavigateDownClicked = this.onNavigateDownClicked.bind( this ); +		this.onNavigatePrevClicked = this.onNavigatePrevClicked.bind( this ); +		this.onNavigateNextClicked = this.onNavigateNextClicked.bind( this ); + +	} + +	render() { + +		const rtl = this.Reveal.getConfig().rtl; +		const revealElement = this.Reveal.getRevealElement(); + +		this.element = document.createElement( 'aside' ); +		this.element.className = 'controls'; +		this.element.innerHTML = +			`<button class="navigate-left" aria-label="${ rtl ? 'next slide' : 'previous slide' }"><div class="controls-arrow"></div></button> +			<button class="navigate-right" aria-label="${ rtl ? 'previous slide' : 'next slide' }"><div class="controls-arrow"></div></button> +			<button class="navigate-up" aria-label="above slide"><div class="controls-arrow"></div></button> +			<button class="navigate-down" aria-label="below slide"><div class="controls-arrow"></div></button>`; + +		this.Reveal.getRevealElement().appendChild( this.element ); + +		// There can be multiple instances of controls throughout the page +		this.controlsLeft = queryAll( revealElement, '.navigate-left' ); +		this.controlsRight = queryAll( revealElement, '.navigate-right' ); +		this.controlsUp = queryAll( revealElement, '.navigate-up' ); +		this.controlsDown = queryAll( revealElement, '.navigate-down' ); +		this.controlsPrev = queryAll( revealElement, '.navigate-prev' ); +		this.controlsNext = queryAll( revealElement, '.navigate-next' ); + +		// The left, right and down arrows in the standard reveal.js controls +		this.controlsRightArrow = this.element.querySelector( '.navigate-right' ); +		this.controlsLeftArrow = this.element.querySelector( '.navigate-left' ); +		this.controlsDownArrow = this.element.querySelector( '.navigate-down' ); + +	} + +	/** +	 * Called when the reveal.js config is updated. +	 */ +	configure( config, oldConfig ) { + +		this.element.style.display = config.controls ? 'block' : 'none'; + +		this.element.setAttribute( 'data-controls-layout', config.controlsLayout ); +		this.element.setAttribute( 'data-controls-back-arrows', config.controlsBackArrows ); + +	} + +	bind() { + +		// Listen to both touch and click events, in case the device +		// supports both +		let pointerEvents = [ 'touchstart', 'click' ]; + +		// Only support touch for Android, fixes double navigations in +		// stock browser +		if( isAndroid ) { +			pointerEvents = [ 'touchstart' ]; +		} + +		pointerEvents.forEach( eventName => { +			this.controlsLeft.forEach( el => el.addEventListener( eventName, this.onNavigateLeftClicked, false ) ); +			this.controlsRight.forEach( el => el.addEventListener( eventName, this.onNavigateRightClicked, false ) ); +			this.controlsUp.forEach( el => el.addEventListener( eventName, this.onNavigateUpClicked, false ) ); +			this.controlsDown.forEach( el => el.addEventListener( eventName, this.onNavigateDownClicked, false ) ); +			this.controlsPrev.forEach( el => el.addEventListener( eventName, this.onNavigatePrevClicked, false ) ); +			this.controlsNext.forEach( el => el.addEventListener( eventName, this.onNavigateNextClicked, false ) ); +		} ); + +	} + +	unbind() { + +		[ 'touchstart', 'click' ].forEach( eventName => { +			this.controlsLeft.forEach( el => el.removeEventListener( eventName, this.onNavigateLeftClicked, false ) ); +			this.controlsRight.forEach( el => el.removeEventListener( eventName, this.onNavigateRightClicked, false ) ); +			this.controlsUp.forEach( el => el.removeEventListener( eventName, this.onNavigateUpClicked, false ) ); +			this.controlsDown.forEach( el => el.removeEventListener( eventName, this.onNavigateDownClicked, false ) ); +			this.controlsPrev.forEach( el => el.removeEventListener( eventName, this.onNavigatePrevClicked, false ) ); +			this.controlsNext.forEach( el => el.removeEventListener( eventName, this.onNavigateNextClicked, false ) ); +		} ); + +	} + +	/** +	 * Updates the state of all control/navigation arrows. +	 */ +	update() { + +		let routes = this.Reveal.availableRoutes(); + +		// Remove the 'enabled' class from all directions +		[...this.controlsLeft, ...this.controlsRight, ...this.controlsUp, ...this.controlsDown, ...this.controlsPrev, ...this.controlsNext].forEach( node => { +			node.classList.remove( 'enabled', 'fragmented' ); + +			// Set 'disabled' attribute on all directions +			node.setAttribute( 'disabled', 'disabled' ); +		} ); + +		// Add the 'enabled' class to the available routes; remove 'disabled' attribute to enable buttons +		if( routes.left ) this.controlsLeft.forEach( el => { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } ); +		if( routes.right ) this.controlsRight.forEach( el => { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } ); +		if( routes.up ) this.controlsUp.forEach( el => { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } ); +		if( routes.down ) this.controlsDown.forEach( el => { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } ); + +		// Prev/next buttons +		if( routes.left || routes.up ) this.controlsPrev.forEach( el => { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } ); +		if( routes.right || routes.down ) this.controlsNext.forEach( el => { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } ); + +		// Highlight fragment directions +		let currentSlide = this.Reveal.getCurrentSlide(); +		if( currentSlide ) { + +			let fragmentsRoutes = this.Reveal.fragments.availableRoutes(); + +			// Always apply fragment decorator to prev/next buttons +			if( fragmentsRoutes.prev ) this.controlsPrev.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } ); +			if( fragmentsRoutes.next ) this.controlsNext.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } ); + +			// Apply fragment decorators to directional buttons based on +			// what slide axis they are in +			if( this.Reveal.isVerticalSlide( currentSlide ) ) { +				if( fragmentsRoutes.prev ) this.controlsUp.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } ); +				if( fragmentsRoutes.next ) this.controlsDown.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } ); +			} +			else { +				if( fragmentsRoutes.prev ) this.controlsLeft.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } ); +				if( fragmentsRoutes.next ) this.controlsRight.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } ); +			} + +		} + +		if( this.Reveal.getConfig().controlsTutorial ) { + +			let indices = this.Reveal.getIndices(); + +			// Highlight control arrows with an animation to ensure +			// that the viewer knows how to navigate +			if( !this.Reveal.hasNavigatedVertically() && routes.down ) { +				this.controlsDownArrow.classList.add( 'highlight' ); +			} +			else { +				this.controlsDownArrow.classList.remove( 'highlight' ); + +				if( this.Reveal.getConfig().rtl ) { + +					if( !this.Reveal.hasNavigatedHorizontally() && routes.left && indices.v === 0 ) { +						this.controlsLeftArrow.classList.add( 'highlight' ); +					} +					else { +						this.controlsLeftArrow.classList.remove( 'highlight' ); +					} + +				} else { + +					if( !this.Reveal.hasNavigatedHorizontally() && routes.right && indices.v === 0 ) { +						this.controlsRightArrow.classList.add( 'highlight' ); +					} +					else { +						this.controlsRightArrow.classList.remove( 'highlight' ); +					} +				} +			} +		} +	} + +	destroy() { + +		this.unbind(); +		this.element.remove(); + +	} + +	/** +	 * Event handlers for navigation control buttons. +	 */ +	onNavigateLeftClicked( event ) { + +		event.preventDefault(); +		this.Reveal.onUserInput(); + +		if( this.Reveal.getConfig().navigationMode === 'linear' ) { +			this.Reveal.prev(); +		} +		else { +			this.Reveal.left(); +		} + +	} + +	onNavigateRightClicked( event ) { + +		event.preventDefault(); +		this.Reveal.onUserInput(); + +		if( this.Reveal.getConfig().navigationMode === 'linear' ) { +			this.Reveal.next(); +		} +		else { +			this.Reveal.right(); +		} + +	} + +	onNavigateUpClicked( event ) { + +		event.preventDefault(); +		this.Reveal.onUserInput(); + +		this.Reveal.up(); + +	} + +	onNavigateDownClicked( event ) { + +		event.preventDefault(); +		this.Reveal.onUserInput(); + +		this.Reveal.down(); + +	} + +	onNavigatePrevClicked( event ) { + +		event.preventDefault(); +		this.Reveal.onUserInput(); + +		this.Reveal.prev(); + +	} + +	onNavigateNextClicked( event ) { + +		event.preventDefault(); +		this.Reveal.onUserInput(); + +		this.Reveal.next(); + +	} + + +}
\ No newline at end of file | 
