Added Slides
This commit is contained in:
		
							
								
								
									
										165
									
								
								js/components/playback.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										165
									
								
								js/components/playback.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,165 @@
 | 
			
		||||
/**
 | 
			
		||||
 * UI component that lets the use control auto-slide
 | 
			
		||||
 * playback via play/pause.
 | 
			
		||||
 */
 | 
			
		||||
export default class Playback {
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * @param {HTMLElement} container The component will append
 | 
			
		||||
	 * itself to this
 | 
			
		||||
	 * @param {function} progressCheck A method which will be
 | 
			
		||||
	 * called frequently to get the current playback progress on
 | 
			
		||||
	 * a range of 0-1
 | 
			
		||||
	 */
 | 
			
		||||
	constructor( container, progressCheck ) {
 | 
			
		||||
 | 
			
		||||
		// Cosmetics
 | 
			
		||||
		this.diameter = 100;
 | 
			
		||||
		this.diameter2 = this.diameter/2;
 | 
			
		||||
		this.thickness = 6;
 | 
			
		||||
 | 
			
		||||
		// Flags if we are currently playing
 | 
			
		||||
		this.playing = false;
 | 
			
		||||
 | 
			
		||||
		// Current progress on a 0-1 range
 | 
			
		||||
		this.progress = 0;
 | 
			
		||||
 | 
			
		||||
		// Used to loop the animation smoothly
 | 
			
		||||
		this.progressOffset = 1;
 | 
			
		||||
 | 
			
		||||
		this.container = container;
 | 
			
		||||
		this.progressCheck = progressCheck;
 | 
			
		||||
 | 
			
		||||
		this.canvas = document.createElement( 'canvas' );
 | 
			
		||||
		this.canvas.className = 'playback';
 | 
			
		||||
		this.canvas.width = this.diameter;
 | 
			
		||||
		this.canvas.height = this.diameter;
 | 
			
		||||
		this.canvas.style.width = this.diameter2 + 'px';
 | 
			
		||||
		this.canvas.style.height = this.diameter2 + 'px';
 | 
			
		||||
		this.context = this.canvas.getContext( '2d' );
 | 
			
		||||
 | 
			
		||||
		this.container.appendChild( this.canvas );
 | 
			
		||||
 | 
			
		||||
		this.render();
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	setPlaying( value ) {
 | 
			
		||||
 | 
			
		||||
		const wasPlaying = this.playing;
 | 
			
		||||
 | 
			
		||||
		this.playing = value;
 | 
			
		||||
 | 
			
		||||
		// Start repainting if we weren't already
 | 
			
		||||
		if( !wasPlaying && this.playing ) {
 | 
			
		||||
			this.animate();
 | 
			
		||||
		}
 | 
			
		||||
		else {
 | 
			
		||||
			this.render();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	animate() {
 | 
			
		||||
 | 
			
		||||
		const progressBefore = this.progress;
 | 
			
		||||
 | 
			
		||||
		this.progress = this.progressCheck();
 | 
			
		||||
 | 
			
		||||
		// When we loop, offset the progress so that it eases
 | 
			
		||||
		// smoothly rather than immediately resetting
 | 
			
		||||
		if( progressBefore > 0.8 && this.progress < 0.2 ) {
 | 
			
		||||
			this.progressOffset = this.progress;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		this.render();
 | 
			
		||||
 | 
			
		||||
		if( this.playing ) {
 | 
			
		||||
			requestAnimationFrame( this.animate.bind( this ) );
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Renders the current progress and playback state.
 | 
			
		||||
	 */
 | 
			
		||||
	render() {
 | 
			
		||||
 | 
			
		||||
		let progress = this.playing ? this.progress : 0,
 | 
			
		||||
			radius = ( this.diameter2 ) - this.thickness,
 | 
			
		||||
			x = this.diameter2,
 | 
			
		||||
			y = this.diameter2,
 | 
			
		||||
			iconSize = 28;
 | 
			
		||||
 | 
			
		||||
		// Ease towards 1
 | 
			
		||||
		this.progressOffset += ( 1 - this.progressOffset ) * 0.1;
 | 
			
		||||
 | 
			
		||||
		const endAngle = ( - Math.PI / 2 ) + ( progress * ( Math.PI * 2 ) );
 | 
			
		||||
		const startAngle = ( - Math.PI / 2 ) + ( this.progressOffset * ( Math.PI * 2 ) );
 | 
			
		||||
 | 
			
		||||
		this.context.save();
 | 
			
		||||
		this.context.clearRect( 0, 0, this.diameter, this.diameter );
 | 
			
		||||
 | 
			
		||||
		// Solid background color
 | 
			
		||||
		this.context.beginPath();
 | 
			
		||||
		this.context.arc( x, y, radius + 4, 0, Math.PI * 2, false );
 | 
			
		||||
		this.context.fillStyle = 'rgba( 0, 0, 0, 0.4 )';
 | 
			
		||||
		this.context.fill();
 | 
			
		||||
 | 
			
		||||
		// Draw progress track
 | 
			
		||||
		this.context.beginPath();
 | 
			
		||||
		this.context.arc( x, y, radius, 0, Math.PI * 2, false );
 | 
			
		||||
		this.context.lineWidth = this.thickness;
 | 
			
		||||
		this.context.strokeStyle = 'rgba( 255, 255, 255, 0.2 )';
 | 
			
		||||
		this.context.stroke();
 | 
			
		||||
 | 
			
		||||
		if( this.playing ) {
 | 
			
		||||
			// Draw progress on top of track
 | 
			
		||||
			this.context.beginPath();
 | 
			
		||||
			this.context.arc( x, y, radius, startAngle, endAngle, false );
 | 
			
		||||
			this.context.lineWidth = this.thickness;
 | 
			
		||||
			this.context.strokeStyle = '#fff';
 | 
			
		||||
			this.context.stroke();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		this.context.translate( x - ( iconSize / 2 ), y - ( iconSize / 2 ) );
 | 
			
		||||
 | 
			
		||||
		// Draw play/pause icons
 | 
			
		||||
		if( this.playing ) {
 | 
			
		||||
			this.context.fillStyle = '#fff';
 | 
			
		||||
			this.context.fillRect( 0, 0, iconSize / 2 - 4, iconSize );
 | 
			
		||||
			this.context.fillRect( iconSize / 2 + 4, 0, iconSize / 2 - 4, iconSize );
 | 
			
		||||
		}
 | 
			
		||||
		else {
 | 
			
		||||
			this.context.beginPath();
 | 
			
		||||
			this.context.translate( 4, 0 );
 | 
			
		||||
			this.context.moveTo( 0, 0 );
 | 
			
		||||
			this.context.lineTo( iconSize - 4, iconSize / 2 );
 | 
			
		||||
			this.context.lineTo( 0, iconSize );
 | 
			
		||||
			this.context.fillStyle = '#fff';
 | 
			
		||||
			this.context.fill();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		this.context.restore();
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	on( type, listener ) {
 | 
			
		||||
		this.canvas.addEventListener( type, listener, false );
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	off( type, listener ) {
 | 
			
		||||
		this.canvas.removeEventListener( type, listener, false );
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	destroy() {
 | 
			
		||||
 | 
			
		||||
		this.playing = false;
 | 
			
		||||
 | 
			
		||||
		if( this.canvas.parentNode ) {
 | 
			
		||||
			this.container.removeChild( this.canvas );
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										293
									
								
								js/config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										293
									
								
								js/config.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,293 @@
 | 
			
		||||
/**
 | 
			
		||||
 * The default reveal.js config object.
 | 
			
		||||
 */
 | 
			
		||||
export default {
 | 
			
		||||
 | 
			
		||||
	// The "normal" size of the presentation, aspect ratio will be preserved
 | 
			
		||||
	// when the presentation is scaled to fit different resolutions
 | 
			
		||||
	width: 960,
 | 
			
		||||
	height: 700,
 | 
			
		||||
 | 
			
		||||
	// Factor of the display size that should remain empty around the content
 | 
			
		||||
	margin: 0.04,
 | 
			
		||||
 | 
			
		||||
	// Bounds for smallest/largest possible scale to apply to content
 | 
			
		||||
	minScale: 0.2,
 | 
			
		||||
	maxScale: 2.0,
 | 
			
		||||
 | 
			
		||||
	// Display presentation control arrows
 | 
			
		||||
	controls: true,
 | 
			
		||||
 | 
			
		||||
	// Help the user learn the controls by providing hints, for example by
 | 
			
		||||
	// bouncing the down arrow when they first encounter a vertical slide
 | 
			
		||||
	controlsTutorial: true,
 | 
			
		||||
 | 
			
		||||
	// Determines where controls appear, "edges" or "bottom-right"
 | 
			
		||||
	controlsLayout: 'bottom-right',
 | 
			
		||||
 | 
			
		||||
	// Visibility rule for backwards navigation arrows; "faded", "hidden"
 | 
			
		||||
	// or "visible"
 | 
			
		||||
	controlsBackArrows: 'faded',
 | 
			
		||||
 | 
			
		||||
	// Display a presentation progress bar
 | 
			
		||||
	progress: true,
 | 
			
		||||
 | 
			
		||||
	// Display the page number of the current slide
 | 
			
		||||
	// - true:    Show slide number
 | 
			
		||||
	// - false:   Hide slide number
 | 
			
		||||
	//
 | 
			
		||||
	// Can optionally be set as a string that specifies the number formatting:
 | 
			
		||||
	// - "h.v":	  Horizontal . vertical slide number (default)
 | 
			
		||||
	// - "h/v":	  Horizontal / vertical slide number
 | 
			
		||||
	// - "c":	  Flattened slide number
 | 
			
		||||
	// - "c/t":	  Flattened slide number / total slides
 | 
			
		||||
	//
 | 
			
		||||
	// Alternatively, you can provide a function that returns the slide
 | 
			
		||||
	// number for the current slide. The function should take in a slide
 | 
			
		||||
	// object and return an array with one string [slideNumber] or
 | 
			
		||||
	// three strings [n1,delimiter,n2]. See #formatSlideNumber().
 | 
			
		||||
	slideNumber: false,
 | 
			
		||||
 | 
			
		||||
	// Can be used to limit the contexts in which the slide number appears
 | 
			
		||||
	// - "all":      Always show the slide number
 | 
			
		||||
	// - "print":    Only when printing to PDF
 | 
			
		||||
	// - "speaker":  Only in the speaker view
 | 
			
		||||
	showSlideNumber: 'all',
 | 
			
		||||
 | 
			
		||||
	// Use 1 based indexing for # links to match slide number (default is zero
 | 
			
		||||
	// based)
 | 
			
		||||
	hashOneBasedIndex: false,
 | 
			
		||||
 | 
			
		||||
	// Add the current slide number to the URL hash so that reloading the
 | 
			
		||||
	// page/copying the URL will return you to the same slide
 | 
			
		||||
	hash: false,
 | 
			
		||||
 | 
			
		||||
	// Flags if we should monitor the hash and change slides accordingly
 | 
			
		||||
	respondToHashChanges: true,
 | 
			
		||||
 | 
			
		||||
	// Push each slide change to the browser history.  Implies `hash: true`
 | 
			
		||||
	history: false,
 | 
			
		||||
 | 
			
		||||
	// Enable keyboard shortcuts for navigation
 | 
			
		||||
	keyboard: true,
 | 
			
		||||
 | 
			
		||||
	// Optional function that blocks keyboard events when retuning false
 | 
			
		||||
	//
 | 
			
		||||
	// If you set this to 'focused', we will only capture keyboard events
 | 
			
		||||
	// for embedded decks when they are in focus
 | 
			
		||||
	keyboardCondition: null,
 | 
			
		||||
 | 
			
		||||
	// Disables the default reveal.js slide layout (scaling and centering)
 | 
			
		||||
	// so that you can use custom CSS layout
 | 
			
		||||
	disableLayout: false,
 | 
			
		||||
 | 
			
		||||
	// Enable the slide overview mode
 | 
			
		||||
	overview: true,
 | 
			
		||||
 | 
			
		||||
	// Vertical centering of slides
 | 
			
		||||
	center: true,
 | 
			
		||||
 | 
			
		||||
	// Enables touch navigation on devices with touch input
 | 
			
		||||
	touch: true,
 | 
			
		||||
 | 
			
		||||
	// Loop the presentation
 | 
			
		||||
	loop: false,
 | 
			
		||||
 | 
			
		||||
	// Change the presentation direction to be RTL
 | 
			
		||||
	rtl: false,
 | 
			
		||||
 | 
			
		||||
	// Changes the behavior of our navigation directions.
 | 
			
		||||
	//
 | 
			
		||||
	// "default"
 | 
			
		||||
	// Left/right arrow keys step between horizontal slides, up/down
 | 
			
		||||
	// arrow keys step between vertical slides. Space key steps through
 | 
			
		||||
	// all slides (both horizontal and vertical).
 | 
			
		||||
	//
 | 
			
		||||
	// "linear"
 | 
			
		||||
	// Removes the up/down arrows. Left/right arrows step through all
 | 
			
		||||
	// slides (both horizontal and vertical).
 | 
			
		||||
	//
 | 
			
		||||
	// "grid"
 | 
			
		||||
	// When this is enabled, stepping left/right from a vertical stack
 | 
			
		||||
	// to an adjacent vertical stack will land you at the same vertical
 | 
			
		||||
	// index.
 | 
			
		||||
	//
 | 
			
		||||
	// Consider a deck with six slides ordered in two vertical stacks:
 | 
			
		||||
	// 1.1    2.1
 | 
			
		||||
	// 1.2    2.2
 | 
			
		||||
	// 1.3    2.3
 | 
			
		||||
	//
 | 
			
		||||
	// If you're on slide 1.3 and navigate right, you will normally move
 | 
			
		||||
	// from 1.3 -> 2.1. If "grid" is used, the same navigation takes you
 | 
			
		||||
	// from 1.3 -> 2.3.
 | 
			
		||||
	navigationMode: 'default',
 | 
			
		||||
 | 
			
		||||
	// Randomizes the order of slides each time the presentation loads
 | 
			
		||||
	shuffle: false,
 | 
			
		||||
 | 
			
		||||
	// Turns fragments on and off globally
 | 
			
		||||
	fragments: true,
 | 
			
		||||
 | 
			
		||||
	// Flags whether to include the current fragment in the URL,
 | 
			
		||||
	// so that reloading brings you to the same fragment position
 | 
			
		||||
	fragmentInURL: true,
 | 
			
		||||
 | 
			
		||||
	// Flags if the presentation is running in an embedded mode,
 | 
			
		||||
	// i.e. contained within a limited portion of the screen
 | 
			
		||||
	embedded: false,
 | 
			
		||||
 | 
			
		||||
	// Flags if we should show a help overlay when the question-mark
 | 
			
		||||
	// key is pressed
 | 
			
		||||
	help: true,
 | 
			
		||||
 | 
			
		||||
	// Flags if it should be possible to pause the presentation (blackout)
 | 
			
		||||
	pause: true,
 | 
			
		||||
 | 
			
		||||
	// Flags if speaker notes should be visible to all viewers
 | 
			
		||||
	showNotes: false,
 | 
			
		||||
 | 
			
		||||
	// Flags if slides with data-visibility="hidden" should be kep visible
 | 
			
		||||
	showHiddenSlides: false,
 | 
			
		||||
 | 
			
		||||
	// Global override for autoplaying embedded media (video/audio/iframe)
 | 
			
		||||
	// - null:   Media will only autoplay if data-autoplay is present
 | 
			
		||||
	// - true:   All media will autoplay, regardless of individual setting
 | 
			
		||||
	// - false:  No media will autoplay, regardless of individual setting
 | 
			
		||||
	autoPlayMedia: null,
 | 
			
		||||
 | 
			
		||||
	// Global override for preloading lazy-loaded iframes
 | 
			
		||||
	// - null:   Iframes with data-src AND data-preload will be loaded when within
 | 
			
		||||
	//           the viewDistance, iframes with only data-src will be loaded when visible
 | 
			
		||||
	// - true:   All iframes with data-src will be loaded when within the viewDistance
 | 
			
		||||
	// - false:  All iframes with data-src will be loaded only when visible
 | 
			
		||||
	preloadIframes: null,
 | 
			
		||||
 | 
			
		||||
	// Can be used to globally disable auto-animation
 | 
			
		||||
	autoAnimate: true,
 | 
			
		||||
 | 
			
		||||
	// Optionally provide a custom element matcher that will be
 | 
			
		||||
	// used to dictate which elements we can animate between.
 | 
			
		||||
	autoAnimateMatcher: null,
 | 
			
		||||
 | 
			
		||||
	// Default settings for our auto-animate transitions, can be
 | 
			
		||||
	// overridden per-slide or per-element via data arguments
 | 
			
		||||
	autoAnimateEasing: 'ease',
 | 
			
		||||
	autoAnimateDuration: 1.0,
 | 
			
		||||
	autoAnimateUnmatched: true,
 | 
			
		||||
 | 
			
		||||
	// CSS properties that can be auto-animated. Position & scale
 | 
			
		||||
	// is matched separately so there's no need to include styles
 | 
			
		||||
	// like top/right/bottom/left, width/height or margin.
 | 
			
		||||
	autoAnimateStyles: [
 | 
			
		||||
		'opacity',
 | 
			
		||||
		'color',
 | 
			
		||||
		'background-color',
 | 
			
		||||
		'padding',
 | 
			
		||||
		'font-size',
 | 
			
		||||
		'line-height',
 | 
			
		||||
		'letter-spacing',
 | 
			
		||||
		'border-width',
 | 
			
		||||
		'border-color',
 | 
			
		||||
		'border-radius',
 | 
			
		||||
		'outline',
 | 
			
		||||
		'outline-offset'
 | 
			
		||||
	],
 | 
			
		||||
 | 
			
		||||
	// Controls automatic progression to the next slide
 | 
			
		||||
	// - 0:      Auto-sliding only happens if the data-autoslide HTML attribute
 | 
			
		||||
	//           is present on the current slide or fragment
 | 
			
		||||
	// - 1+:     All slides will progress automatically at the given interval
 | 
			
		||||
	// - false:  No auto-sliding, even if data-autoslide is present
 | 
			
		||||
	autoSlide: 0,
 | 
			
		||||
 | 
			
		||||
	// Stop auto-sliding after user input
 | 
			
		||||
	autoSlideStoppable: true,
 | 
			
		||||
 | 
			
		||||
	// Use this method for navigation when auto-sliding (defaults to navigateNext)
 | 
			
		||||
	autoSlideMethod: null,
 | 
			
		||||
 | 
			
		||||
	// Specify the average time in seconds that you think you will spend
 | 
			
		||||
	// presenting each slide. This is used to show a pacing timer in the
 | 
			
		||||
	// speaker view
 | 
			
		||||
	defaultTiming: null,
 | 
			
		||||
 | 
			
		||||
	// Enable slide navigation via mouse wheel
 | 
			
		||||
	mouseWheel: false,
 | 
			
		||||
 | 
			
		||||
	// Opens links in an iframe preview overlay
 | 
			
		||||
	// Add `data-preview-link` and `data-preview-link="false"` to customise each link
 | 
			
		||||
	// individually
 | 
			
		||||
	previewLinks: false,
 | 
			
		||||
 | 
			
		||||
	// Exposes the reveal.js API through window.postMessage
 | 
			
		||||
	postMessage: true,
 | 
			
		||||
 | 
			
		||||
	// Dispatches all reveal.js events to the parent window through postMessage
 | 
			
		||||
	postMessageEvents: false,
 | 
			
		||||
 | 
			
		||||
	// Focuses body when page changes visibility to ensure keyboard shortcuts work
 | 
			
		||||
	focusBodyOnPageVisibilityChange: true,
 | 
			
		||||
 | 
			
		||||
	// Transition style
 | 
			
		||||
	transition: 'slide', // none/fade/slide/convex/concave/zoom
 | 
			
		||||
 | 
			
		||||
	// Transition speed
 | 
			
		||||
	transitionSpeed: 'default', // default/fast/slow
 | 
			
		||||
 | 
			
		||||
	// Transition style for full page slide backgrounds
 | 
			
		||||
	backgroundTransition: 'fade', // none/fade/slide/convex/concave/zoom
 | 
			
		||||
 | 
			
		||||
	// Parallax background image
 | 
			
		||||
	parallaxBackgroundImage: '', // CSS syntax, e.g. "a.jpg"
 | 
			
		||||
 | 
			
		||||
	// Parallax background size
 | 
			
		||||
	parallaxBackgroundSize: '', // CSS syntax, e.g. "3000px 2000px"
 | 
			
		||||
 | 
			
		||||
	// Parallax background repeat
 | 
			
		||||
	parallaxBackgroundRepeat: '', // repeat/repeat-x/repeat-y/no-repeat/initial/inherit
 | 
			
		||||
 | 
			
		||||
	// Parallax background position
 | 
			
		||||
	parallaxBackgroundPosition: '', // CSS syntax, e.g. "top left"
 | 
			
		||||
 | 
			
		||||
	// Amount of pixels to move the parallax background per slide step
 | 
			
		||||
	parallaxBackgroundHorizontal: null,
 | 
			
		||||
	parallaxBackgroundVertical: null,
 | 
			
		||||
 | 
			
		||||
	// The maximum number of pages a single slide can expand onto when printing
 | 
			
		||||
	// to PDF, unlimited by default
 | 
			
		||||
	pdfMaxPagesPerSlide: Number.POSITIVE_INFINITY,
 | 
			
		||||
 | 
			
		||||
	// Prints each fragment on a separate slide
 | 
			
		||||
	pdfSeparateFragments: true,
 | 
			
		||||
 | 
			
		||||
	// Offset used to reduce the height of content within exported PDF pages.
 | 
			
		||||
	// This exists to account for environment differences based on how you
 | 
			
		||||
	// print to PDF. CLI printing options, like phantomjs and wkpdf, can end
 | 
			
		||||
	// on precisely the total height of the document whereas in-browser
 | 
			
		||||
	// printing has to end one pixel before.
 | 
			
		||||
	pdfPageHeightOffset: -1,
 | 
			
		||||
 | 
			
		||||
	// Number of slides away from the current that are visible
 | 
			
		||||
	viewDistance: 3,
 | 
			
		||||
 | 
			
		||||
	// Number of slides away from the current that are visible on mobile
 | 
			
		||||
	// devices. It is advisable to set this to a lower number than
 | 
			
		||||
	// viewDistance in order to save resources.
 | 
			
		||||
	mobileViewDistance: 2,
 | 
			
		||||
 | 
			
		||||
	// The display mode that will be used to show slides
 | 
			
		||||
	display: 'block',
 | 
			
		||||
 | 
			
		||||
	// Hide cursor if inactive
 | 
			
		||||
	hideInactiveCursor: true,
 | 
			
		||||
 | 
			
		||||
	// Time before the cursor is hidden (in ms)
 | 
			
		||||
	hideCursorTime: 5000,
 | 
			
		||||
 | 
			
		||||
	// Script dependencies to load
 | 
			
		||||
	dependencies: [],
 | 
			
		||||
 | 
			
		||||
	// Plugin objects to register and use for this presentation
 | 
			
		||||
	plugins: []
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										634
									
								
								js/controllers/autoanimate.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										634
									
								
								js/controllers/autoanimate.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,634 @@
 | 
			
		||||
import { queryAll, extend, createStyleSheet, matches, closest } from '../utils/util.js'
 | 
			
		||||
import { FRAGMENT_STYLE_REGEX } from '../utils/constants.js'
 | 
			
		||||
 | 
			
		||||
// Counter used to generate unique IDs for auto-animated elements
 | 
			
		||||
let autoAnimateCounter = 0;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Automatically animates matching elements across
 | 
			
		||||
 * slides with the [data-auto-animate] attribute.
 | 
			
		||||
 */
 | 
			
		||||
export default class AutoAnimate {
 | 
			
		||||
 | 
			
		||||
	constructor( Reveal ) {
 | 
			
		||||
 | 
			
		||||
		this.Reveal = Reveal;
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Runs an auto-animation between the given slides.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param  {HTMLElement} fromSlide
 | 
			
		||||
	 * @param  {HTMLElement} toSlide
 | 
			
		||||
	 */
 | 
			
		||||
	run( fromSlide, toSlide ) {
 | 
			
		||||
 | 
			
		||||
		// Clean up after prior animations
 | 
			
		||||
		this.reset();
 | 
			
		||||
 | 
			
		||||
		let allSlides = this.Reveal.getSlides();
 | 
			
		||||
		let toSlideIndex = allSlides.indexOf( toSlide );
 | 
			
		||||
		let fromSlideIndex = allSlides.indexOf( fromSlide );
 | 
			
		||||
 | 
			
		||||
		// Ensure that both slides are auto-animate targets with the same data-auto-animate-id value
 | 
			
		||||
		// (including null if absent on both) and that data-auto-animate-restart isn't set on the
 | 
			
		||||
		// physically latter slide (independent of slide direction)
 | 
			
		||||
		if( fromSlide.hasAttribute( 'data-auto-animate' ) && toSlide.hasAttribute( 'data-auto-animate' )
 | 
			
		||||
				&& fromSlide.getAttribute( 'data-auto-animate-id' ) === toSlide.getAttribute( 'data-auto-animate-id' ) 
 | 
			
		||||
				&& !( toSlideIndex > fromSlideIndex ? toSlide : fromSlide ).hasAttribute( 'data-auto-animate-restart' ) ) {
 | 
			
		||||
 | 
			
		||||
			// Create a new auto-animate sheet
 | 
			
		||||
			this.autoAnimateStyleSheet = this.autoAnimateStyleSheet || createStyleSheet();
 | 
			
		||||
 | 
			
		||||
			let animationOptions = this.getAutoAnimateOptions( toSlide );
 | 
			
		||||
 | 
			
		||||
			// Set our starting state
 | 
			
		||||
			fromSlide.dataset.autoAnimate = 'pending';
 | 
			
		||||
			toSlide.dataset.autoAnimate = 'pending';
 | 
			
		||||
 | 
			
		||||
			// Flag the navigation direction, needed for fragment buildup
 | 
			
		||||
			animationOptions.slideDirection = toSlideIndex > fromSlideIndex ? 'forward' : 'backward';
 | 
			
		||||
 | 
			
		||||
			// If the from-slide is hidden because it has moved outside
 | 
			
		||||
			// the view distance, we need to temporarily show it while
 | 
			
		||||
			// measuring
 | 
			
		||||
			let fromSlideIsHidden = fromSlide.style.display === 'none';
 | 
			
		||||
			if( fromSlideIsHidden ) fromSlide.style.display = this.Reveal.getConfig().display;
 | 
			
		||||
 | 
			
		||||
			// Inject our auto-animate styles for this transition
 | 
			
		||||
			let css = this.getAutoAnimatableElements( fromSlide, toSlide ).map( elements => {
 | 
			
		||||
				return this.autoAnimateElements( elements.from, elements.to, elements.options || {}, animationOptions, autoAnimateCounter++ );
 | 
			
		||||
			} );
 | 
			
		||||
 | 
			
		||||
			if( fromSlideIsHidden ) fromSlide.style.display = 'none';
 | 
			
		||||
 | 
			
		||||
			// Animate unmatched elements, if enabled
 | 
			
		||||
			if( toSlide.dataset.autoAnimateUnmatched !== 'false' && this.Reveal.getConfig().autoAnimateUnmatched === true ) {
 | 
			
		||||
 | 
			
		||||
				// Our default timings for unmatched elements
 | 
			
		||||
				let defaultUnmatchedDuration = animationOptions.duration * 0.8,
 | 
			
		||||
					defaultUnmatchedDelay = animationOptions.duration * 0.2;
 | 
			
		||||
 | 
			
		||||
				this.getUnmatchedAutoAnimateElements( toSlide ).forEach( unmatchedElement => {
 | 
			
		||||
 | 
			
		||||
					let unmatchedOptions = this.getAutoAnimateOptions( unmatchedElement, animationOptions );
 | 
			
		||||
					let id = 'unmatched';
 | 
			
		||||
 | 
			
		||||
					// If there is a duration or delay set specifically for this
 | 
			
		||||
					// element our unmatched elements should adhere to those
 | 
			
		||||
					if( unmatchedOptions.duration !== animationOptions.duration || unmatchedOptions.delay !== animationOptions.delay ) {
 | 
			
		||||
						id = 'unmatched-' + autoAnimateCounter++;
 | 
			
		||||
						css.push( `[data-auto-animate="running"] [data-auto-animate-target="${id}"] { transition: opacity ${unmatchedOptions.duration}s ease ${unmatchedOptions.delay}s; }` );
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					unmatchedElement.dataset.autoAnimateTarget = id;
 | 
			
		||||
 | 
			
		||||
				}, this );
 | 
			
		||||
 | 
			
		||||
				// Our default transition for unmatched elements
 | 
			
		||||
				css.push( `[data-auto-animate="running"] [data-auto-animate-target="unmatched"] { transition: opacity ${defaultUnmatchedDuration}s ease ${defaultUnmatchedDelay}s; }` );
 | 
			
		||||
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// Setting the whole chunk of CSS at once is the most
 | 
			
		||||
			// efficient way to do this. Using sheet.insertRule
 | 
			
		||||
			// is multiple factors slower.
 | 
			
		||||
			this.autoAnimateStyleSheet.innerHTML = css.join( '' );
 | 
			
		||||
 | 
			
		||||
			// Start the animation next cycle
 | 
			
		||||
			requestAnimationFrame( () => {
 | 
			
		||||
				if( this.autoAnimateStyleSheet ) {
 | 
			
		||||
					// This forces our newly injected styles to be applied in Firefox
 | 
			
		||||
					getComputedStyle( this.autoAnimateStyleSheet ).fontWeight;
 | 
			
		||||
 | 
			
		||||
					toSlide.dataset.autoAnimate = 'running';
 | 
			
		||||
				}
 | 
			
		||||
			} );
 | 
			
		||||
 | 
			
		||||
			this.Reveal.dispatchEvent({
 | 
			
		||||
				type: 'autoanimate',
 | 
			
		||||
				data: {
 | 
			
		||||
					fromSlide,
 | 
			
		||||
					toSlide,
 | 
			
		||||
					sheet: this.autoAnimateStyleSheet
 | 
			
		||||
				}
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Rolls back all changes that we've made to the DOM so
 | 
			
		||||
	 * that as part of animating.
 | 
			
		||||
	 */
 | 
			
		||||
	reset() {
 | 
			
		||||
 | 
			
		||||
		// Reset slides
 | 
			
		||||
		queryAll( this.Reveal.getRevealElement(), '[data-auto-animate]:not([data-auto-animate=""])' ).forEach( element => {
 | 
			
		||||
			element.dataset.autoAnimate = '';
 | 
			
		||||
		} );
 | 
			
		||||
 | 
			
		||||
		// Reset elements
 | 
			
		||||
		queryAll( this.Reveal.getRevealElement(), '[data-auto-animate-target]' ).forEach( element => {
 | 
			
		||||
			delete element.dataset.autoAnimateTarget;
 | 
			
		||||
		} );
 | 
			
		||||
 | 
			
		||||
		// Remove the animation sheet
 | 
			
		||||
		if( this.autoAnimateStyleSheet && this.autoAnimateStyleSheet.parentNode ) {
 | 
			
		||||
			this.autoAnimateStyleSheet.parentNode.removeChild( this.autoAnimateStyleSheet );
 | 
			
		||||
			this.autoAnimateStyleSheet = null;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Creates a FLIP animation where the `to` element starts out
 | 
			
		||||
	 * in the `from` element position and animates to its original
 | 
			
		||||
	 * state.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param {HTMLElement} from
 | 
			
		||||
	 * @param {HTMLElement} to
 | 
			
		||||
	 * @param {Object} elementOptions Options for this element pair
 | 
			
		||||
	 * @param {Object} animationOptions Options set at the slide level
 | 
			
		||||
	 * @param {String} id Unique ID that we can use to identify this
 | 
			
		||||
	 * auto-animate element in the DOM
 | 
			
		||||
	 */
 | 
			
		||||
	autoAnimateElements( from, to, elementOptions, animationOptions, id ) {
 | 
			
		||||
 | 
			
		||||
		// 'from' elements are given a data-auto-animate-target with no value,
 | 
			
		||||
		// 'to' elements are are given a data-auto-animate-target with an ID
 | 
			
		||||
		from.dataset.autoAnimateTarget = '';
 | 
			
		||||
		to.dataset.autoAnimateTarget = id;
 | 
			
		||||
 | 
			
		||||
		// Each element may override any of the auto-animate options
 | 
			
		||||
		// like transition easing, duration and delay via data-attributes
 | 
			
		||||
		let options = this.getAutoAnimateOptions( to, animationOptions );
 | 
			
		||||
 | 
			
		||||
		// If we're using a custom element matcher the element options
 | 
			
		||||
		// may contain additional transition overrides
 | 
			
		||||
		if( typeof elementOptions.delay !== 'undefined' ) options.delay = elementOptions.delay;
 | 
			
		||||
		if( typeof elementOptions.duration !== 'undefined' ) options.duration = elementOptions.duration;
 | 
			
		||||
		if( typeof elementOptions.easing !== 'undefined' ) options.easing = elementOptions.easing;
 | 
			
		||||
 | 
			
		||||
		let fromProps = this.getAutoAnimatableProperties( 'from', from, elementOptions ),
 | 
			
		||||
			toProps = this.getAutoAnimatableProperties( 'to', to, elementOptions );
 | 
			
		||||
 | 
			
		||||
		// Maintain fragment visibility for matching elements when
 | 
			
		||||
		// we're navigating forwards, this way the viewer won't need
 | 
			
		||||
		// to step through the same fragments twice
 | 
			
		||||
		if( to.classList.contains( 'fragment' ) ) {
 | 
			
		||||
 | 
			
		||||
			// Don't auto-animate the opacity of fragments to avoid
 | 
			
		||||
			// conflicts with fragment animations
 | 
			
		||||
			delete toProps.styles['opacity'];
 | 
			
		||||
 | 
			
		||||
			if( from.classList.contains( 'fragment' ) ) {
 | 
			
		||||
 | 
			
		||||
				let fromFragmentStyle = ( from.className.match( FRAGMENT_STYLE_REGEX ) || [''] )[0];
 | 
			
		||||
				let toFragmentStyle = ( to.className.match( FRAGMENT_STYLE_REGEX ) || [''] )[0];
 | 
			
		||||
 | 
			
		||||
				// Only skip the fragment if the fragment animation style
 | 
			
		||||
				// remains unchanged
 | 
			
		||||
				if( fromFragmentStyle === toFragmentStyle && animationOptions.slideDirection === 'forward' ) {
 | 
			
		||||
					to.classList.add( 'visible', 'disabled' );
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// If translation and/or scaling are enabled, css transform
 | 
			
		||||
		// the 'to' element so that it matches the position and size
 | 
			
		||||
		// of the 'from' element
 | 
			
		||||
		if( elementOptions.translate !== false || elementOptions.scale !== false ) {
 | 
			
		||||
 | 
			
		||||
			let presentationScale = this.Reveal.getScale();
 | 
			
		||||
 | 
			
		||||
			let delta = {
 | 
			
		||||
				x: ( fromProps.x - toProps.x ) / presentationScale,
 | 
			
		||||
				y: ( fromProps.y - toProps.y ) / presentationScale,
 | 
			
		||||
				scaleX: fromProps.width / toProps.width,
 | 
			
		||||
				scaleY: fromProps.height / toProps.height
 | 
			
		||||
			};
 | 
			
		||||
 | 
			
		||||
			// Limit decimal points to avoid 0.0001px blur and stutter
 | 
			
		||||
			delta.x = Math.round( delta.x * 1000 ) / 1000;
 | 
			
		||||
			delta.y = Math.round( delta.y * 1000 ) / 1000;
 | 
			
		||||
			delta.scaleX = Math.round( delta.scaleX * 1000 ) / 1000;
 | 
			
		||||
			delta.scaleX = Math.round( delta.scaleX * 1000 ) / 1000;
 | 
			
		||||
 | 
			
		||||
			let translate = elementOptions.translate !== false && ( delta.x !== 0 || delta.y !== 0 ),
 | 
			
		||||
				scale = elementOptions.scale !== false && ( delta.scaleX !== 0 || delta.scaleY !== 0 );
 | 
			
		||||
 | 
			
		||||
			// No need to transform if nothing's changed
 | 
			
		||||
			if( translate || scale ) {
 | 
			
		||||
 | 
			
		||||
				let transform = [];
 | 
			
		||||
 | 
			
		||||
				if( translate ) transform.push( `translate(${delta.x}px, ${delta.y}px)` );
 | 
			
		||||
				if( scale ) transform.push( `scale(${delta.scaleX}, ${delta.scaleY})` );
 | 
			
		||||
 | 
			
		||||
				fromProps.styles['transform'] = transform.join( ' ' );
 | 
			
		||||
				fromProps.styles['transform-origin'] = 'top left';
 | 
			
		||||
 | 
			
		||||
				toProps.styles['transform'] = 'none';
 | 
			
		||||
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Delete all unchanged 'to' styles
 | 
			
		||||
		for( let propertyName in toProps.styles ) {
 | 
			
		||||
			const toValue = toProps.styles[propertyName];
 | 
			
		||||
			const fromValue = fromProps.styles[propertyName];
 | 
			
		||||
 | 
			
		||||
			if( toValue === fromValue ) {
 | 
			
		||||
				delete toProps.styles[propertyName];
 | 
			
		||||
			}
 | 
			
		||||
			else {
 | 
			
		||||
				// If these property values were set via a custom matcher providing
 | 
			
		||||
				// an explicit 'from' and/or 'to' value, we always inject those values.
 | 
			
		||||
				if( toValue.explicitValue === true ) {
 | 
			
		||||
					toProps.styles[propertyName] = toValue.value;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				if( fromValue.explicitValue === true ) {
 | 
			
		||||
					fromProps.styles[propertyName] = fromValue.value;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		let css = '';
 | 
			
		||||
 | 
			
		||||
		let toStyleProperties = Object.keys( toProps.styles );
 | 
			
		||||
 | 
			
		||||
		// Only create animate this element IF at least one style
 | 
			
		||||
		// property has changed
 | 
			
		||||
		if( toStyleProperties.length > 0 ) {
 | 
			
		||||
 | 
			
		||||
			// Instantly move to the 'from' state
 | 
			
		||||
			fromProps.styles['transition'] = 'none';
 | 
			
		||||
 | 
			
		||||
			// Animate towards the 'to' state
 | 
			
		||||
			toProps.styles['transition'] = `all ${options.duration}s ${options.easing} ${options.delay}s`;
 | 
			
		||||
			toProps.styles['transition-property'] = toStyleProperties.join( ', ' );
 | 
			
		||||
			toProps.styles['will-change'] = toStyleProperties.join( ', ' );
 | 
			
		||||
 | 
			
		||||
			// Build up our custom CSS. We need to override inline styles
 | 
			
		||||
			// so we need to make our styles vErY IMPORTANT!1!!
 | 
			
		||||
			let fromCSS = Object.keys( fromProps.styles ).map( propertyName => {
 | 
			
		||||
				return propertyName + ': ' + fromProps.styles[propertyName] + ' !important;';
 | 
			
		||||
			} ).join( '' );
 | 
			
		||||
 | 
			
		||||
			let toCSS = Object.keys( toProps.styles ).map( propertyName => {
 | 
			
		||||
				return propertyName + ': ' + toProps.styles[propertyName] + ' !important;';
 | 
			
		||||
			} ).join( '' );
 | 
			
		||||
 | 
			
		||||
			css = 	'[data-auto-animate-target="'+ id +'"] {'+ fromCSS +'}' +
 | 
			
		||||
					'[data-auto-animate="running"] [data-auto-animate-target="'+ id +'"] {'+ toCSS +'}';
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return css;
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Returns the auto-animate options for the given element.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param {HTMLElement} element Element to pick up options
 | 
			
		||||
	 * from, either a slide or an animation target
 | 
			
		||||
	 * @param {Object} [inheritedOptions] Optional set of existing
 | 
			
		||||
	 * options
 | 
			
		||||
	 */
 | 
			
		||||
	getAutoAnimateOptions( element, inheritedOptions ) {
 | 
			
		||||
 | 
			
		||||
		let options = {
 | 
			
		||||
			easing: this.Reveal.getConfig().autoAnimateEasing,
 | 
			
		||||
			duration: this.Reveal.getConfig().autoAnimateDuration,
 | 
			
		||||
			delay: 0
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		options = extend( options, inheritedOptions );
 | 
			
		||||
 | 
			
		||||
		// Inherit options from parent elements
 | 
			
		||||
		if( element.parentNode ) {
 | 
			
		||||
			let autoAnimatedParent = closest( element.parentNode, '[data-auto-animate-target]' );
 | 
			
		||||
			if( autoAnimatedParent ) {
 | 
			
		||||
				options = this.getAutoAnimateOptions( autoAnimatedParent, options );
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if( element.dataset.autoAnimateEasing ) {
 | 
			
		||||
			options.easing = element.dataset.autoAnimateEasing;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if( element.dataset.autoAnimateDuration ) {
 | 
			
		||||
			options.duration = parseFloat( element.dataset.autoAnimateDuration );
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if( element.dataset.autoAnimateDelay ) {
 | 
			
		||||
			options.delay = parseFloat( element.dataset.autoAnimateDelay );
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return options;
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Returns an object containing all of the properties
 | 
			
		||||
	 * that can be auto-animated for the given element and
 | 
			
		||||
	 * their current computed values.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param {String} direction 'from' or 'to'
 | 
			
		||||
	 */
 | 
			
		||||
	getAutoAnimatableProperties( direction, element, elementOptions ) {
 | 
			
		||||
 | 
			
		||||
		let config = this.Reveal.getConfig();
 | 
			
		||||
 | 
			
		||||
		let properties = { styles: [] };
 | 
			
		||||
 | 
			
		||||
		// Position and size
 | 
			
		||||
		if( elementOptions.translate !== false || elementOptions.scale !== false ) {
 | 
			
		||||
			let bounds;
 | 
			
		||||
 | 
			
		||||
			// Custom auto-animate may optionally return a custom tailored
 | 
			
		||||
			// measurement function
 | 
			
		||||
			if( typeof elementOptions.measure === 'function' ) {
 | 
			
		||||
				bounds = elementOptions.measure( element );
 | 
			
		||||
			}
 | 
			
		||||
			else {
 | 
			
		||||
				if( config.center ) {
 | 
			
		||||
					// More precise, but breaks when used in combination
 | 
			
		||||
					// with zoom for scaling the deck ¯\_(ツ)_/¯
 | 
			
		||||
					bounds = element.getBoundingClientRect();
 | 
			
		||||
				}
 | 
			
		||||
				else {
 | 
			
		||||
					let scale = this.Reveal.getScale();
 | 
			
		||||
					bounds = {
 | 
			
		||||
						x: element.offsetLeft * scale,
 | 
			
		||||
						y: element.offsetTop * scale,
 | 
			
		||||
						width: element.offsetWidth * scale,
 | 
			
		||||
						height: element.offsetHeight * scale
 | 
			
		||||
					};
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			properties.x = bounds.x;
 | 
			
		||||
			properties.y = bounds.y;
 | 
			
		||||
			properties.width = bounds.width;
 | 
			
		||||
			properties.height = bounds.height;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const computedStyles = getComputedStyle( element );
 | 
			
		||||
 | 
			
		||||
		// CSS styles
 | 
			
		||||
		( elementOptions.styles || config.autoAnimateStyles ).forEach( style => {
 | 
			
		||||
			let value;
 | 
			
		||||
 | 
			
		||||
			// `style` is either the property name directly, or an object
 | 
			
		||||
			// definition of a style property
 | 
			
		||||
			if( typeof style === 'string' ) style = { property: style };
 | 
			
		||||
 | 
			
		||||
			if( typeof style.from !== 'undefined' && direction === 'from' ) {
 | 
			
		||||
				value = { value: style.from, explicitValue: true };
 | 
			
		||||
			}
 | 
			
		||||
			else if( typeof style.to !== 'undefined' && direction === 'to' ) {
 | 
			
		||||
				value = { value: style.to, explicitValue: true };
 | 
			
		||||
			}
 | 
			
		||||
			else {
 | 
			
		||||
				value = computedStyles[style.property];
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if( value !== '' ) {
 | 
			
		||||
				properties.styles[style.property] = value;
 | 
			
		||||
			}
 | 
			
		||||
		} );
 | 
			
		||||
 | 
			
		||||
		return properties;
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Get a list of all element pairs that we can animate
 | 
			
		||||
	 * between the given slides.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param {HTMLElement} fromSlide
 | 
			
		||||
	 * @param {HTMLElement} toSlide
 | 
			
		||||
	 *
 | 
			
		||||
	 * @return {Array} Each value is an array where [0] is
 | 
			
		||||
	 * the element we're animating from and [1] is the
 | 
			
		||||
	 * element we're animating to
 | 
			
		||||
	 */
 | 
			
		||||
	getAutoAnimatableElements( fromSlide, toSlide ) {
 | 
			
		||||
 | 
			
		||||
		let matcher = typeof this.Reveal.getConfig().autoAnimateMatcher === 'function' ? this.Reveal.getConfig().autoAnimateMatcher : this.getAutoAnimatePairs;
 | 
			
		||||
 | 
			
		||||
		let pairs = matcher.call( this, fromSlide, toSlide );
 | 
			
		||||
 | 
			
		||||
		let reserved = [];
 | 
			
		||||
 | 
			
		||||
		// Remove duplicate pairs
 | 
			
		||||
		return pairs.filter( ( pair, index ) => {
 | 
			
		||||
			if( reserved.indexOf( pair.to ) === -1 ) {
 | 
			
		||||
				reserved.push( pair.to );
 | 
			
		||||
				return true;
 | 
			
		||||
			}
 | 
			
		||||
		} );
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Identifies matching elements between slides.
 | 
			
		||||
	 *
 | 
			
		||||
	 * You can specify a custom matcher function by using
 | 
			
		||||
	 * the `autoAnimateMatcher` config option.
 | 
			
		||||
	 */
 | 
			
		||||
	getAutoAnimatePairs( fromSlide, toSlide ) {
 | 
			
		||||
 | 
			
		||||
		let pairs = [];
 | 
			
		||||
 | 
			
		||||
		const codeNodes = 'pre';
 | 
			
		||||
		const textNodes = 'h1, h2, h3, h4, h5, h6, p, li';
 | 
			
		||||
		const mediaNodes = 'img, video, iframe';
 | 
			
		||||
 | 
			
		||||
		// Eplicit matches via data-id
 | 
			
		||||
		this.findAutoAnimateMatches( pairs, fromSlide, toSlide, '[data-id]', node => {
 | 
			
		||||
			return node.nodeName + ':::' + node.getAttribute( 'data-id' );
 | 
			
		||||
		} );
 | 
			
		||||
 | 
			
		||||
		// Text
 | 
			
		||||
		this.findAutoAnimateMatches( pairs, fromSlide, toSlide, textNodes, node => {
 | 
			
		||||
			return node.nodeName + ':::' + node.innerText;
 | 
			
		||||
		} );
 | 
			
		||||
 | 
			
		||||
		// Media
 | 
			
		||||
		this.findAutoAnimateMatches( pairs, fromSlide, toSlide, mediaNodes, node => {
 | 
			
		||||
			return node.nodeName + ':::' + ( node.getAttribute( 'src' ) || node.getAttribute( 'data-src' ) );
 | 
			
		||||
		} );
 | 
			
		||||
 | 
			
		||||
		// Code
 | 
			
		||||
		this.findAutoAnimateMatches( pairs, fromSlide, toSlide, codeNodes, node => {
 | 
			
		||||
			return node.nodeName + ':::' + node.innerText;
 | 
			
		||||
		} );
 | 
			
		||||
 | 
			
		||||
		pairs.forEach( pair => {
 | 
			
		||||
 | 
			
		||||
			// Disable scale transformations on text nodes, we transition
 | 
			
		||||
			// each individual text property instead
 | 
			
		||||
			if( matches( pair.from, textNodes ) ) {
 | 
			
		||||
				pair.options = { scale: false };
 | 
			
		||||
			}
 | 
			
		||||
			// Animate individual lines of code
 | 
			
		||||
			else if( matches( pair.from, codeNodes ) ) {
 | 
			
		||||
 | 
			
		||||
				// Transition the code block's width and height instead of scaling
 | 
			
		||||
				// to prevent its content from being squished
 | 
			
		||||
				pair.options = { scale: false, styles: [ 'width', 'height' ] };
 | 
			
		||||
 | 
			
		||||
				// Lines of code
 | 
			
		||||
				this.findAutoAnimateMatches( pairs, pair.from, pair.to, '.hljs .hljs-ln-code', node => {
 | 
			
		||||
					return node.textContent;
 | 
			
		||||
				}, {
 | 
			
		||||
					scale: false,
 | 
			
		||||
					styles: [],
 | 
			
		||||
					measure: this.getLocalBoundingBox.bind( this )
 | 
			
		||||
				} );
 | 
			
		||||
 | 
			
		||||
				// Line numbers
 | 
			
		||||
				this.findAutoAnimateMatches( pairs, pair.from, pair.to, '.hljs .hljs-ln-line[data-line-number]', node => {
 | 
			
		||||
					return node.getAttribute( 'data-line-number' );
 | 
			
		||||
				}, {
 | 
			
		||||
					scale: false,
 | 
			
		||||
					styles: [ 'width' ],
 | 
			
		||||
					measure: this.getLocalBoundingBox.bind( this )
 | 
			
		||||
				} );
 | 
			
		||||
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
		}, this );
 | 
			
		||||
 | 
			
		||||
		return pairs;
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Helper method which returns a bounding box based on
 | 
			
		||||
	 * the given elements offset coordinates.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param {HTMLElement} element
 | 
			
		||||
	 * @return {Object} x, y, width, height
 | 
			
		||||
	 */
 | 
			
		||||
	getLocalBoundingBox( element ) {
 | 
			
		||||
 | 
			
		||||
		const presentationScale = this.Reveal.getScale();
 | 
			
		||||
 | 
			
		||||
		return {
 | 
			
		||||
			x: Math.round( ( element.offsetLeft * presentationScale ) * 100 ) / 100,
 | 
			
		||||
			y: Math.round( ( element.offsetTop * presentationScale ) * 100 ) / 100,
 | 
			
		||||
			width: Math.round( ( element.offsetWidth * presentationScale ) * 100 ) / 100,
 | 
			
		||||
			height: Math.round( ( element.offsetHeight * presentationScale ) * 100 ) / 100
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Finds matching elements between two slides.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param {Array} pairs            	List of pairs to push matches to
 | 
			
		||||
	 * @param {HTMLElement} fromScope   Scope within the from element exists
 | 
			
		||||
	 * @param {HTMLElement} toScope     Scope within the to element exists
 | 
			
		||||
	 * @param {String} selector         CSS selector of the element to match
 | 
			
		||||
	 * @param {Function} serializer     A function that accepts an element and returns
 | 
			
		||||
	 *                                  a stringified ID based on its contents
 | 
			
		||||
	 * @param {Object} animationOptions Optional config options for this pair
 | 
			
		||||
	 */
 | 
			
		||||
	findAutoAnimateMatches( pairs, fromScope, toScope, selector, serializer, animationOptions ) {
 | 
			
		||||
 | 
			
		||||
		let fromMatches = {};
 | 
			
		||||
		let toMatches = {};
 | 
			
		||||
 | 
			
		||||
		[].slice.call( fromScope.querySelectorAll( selector ) ).forEach( ( element, i ) => {
 | 
			
		||||
			const key = serializer( element );
 | 
			
		||||
			if( typeof key === 'string' && key.length ) {
 | 
			
		||||
				fromMatches[key] = fromMatches[key] || [];
 | 
			
		||||
				fromMatches[key].push( element );
 | 
			
		||||
			}
 | 
			
		||||
		} );
 | 
			
		||||
 | 
			
		||||
		[].slice.call( toScope.querySelectorAll( selector ) ).forEach( ( element, i ) => {
 | 
			
		||||
			const key = serializer( element );
 | 
			
		||||
			toMatches[key] = toMatches[key] || [];
 | 
			
		||||
			toMatches[key].push( element );
 | 
			
		||||
 | 
			
		||||
			let fromElement;
 | 
			
		||||
 | 
			
		||||
			// Retrieve the 'from' element
 | 
			
		||||
			if( fromMatches[key] ) {
 | 
			
		||||
				const pimaryIndex = toMatches[key].length - 1;
 | 
			
		||||
				const secondaryIndex = fromMatches[key].length - 1;
 | 
			
		||||
 | 
			
		||||
				// If there are multiple identical from elements, retrieve
 | 
			
		||||
				// the one at the same index as our to-element.
 | 
			
		||||
				if( fromMatches[key][ pimaryIndex ] ) {
 | 
			
		||||
					fromElement = fromMatches[key][ pimaryIndex ];
 | 
			
		||||
					fromMatches[key][ pimaryIndex ] = null;
 | 
			
		||||
				}
 | 
			
		||||
				// If there are no matching from-elements at the same index,
 | 
			
		||||
				// use the last one.
 | 
			
		||||
				else if( fromMatches[key][ secondaryIndex ] ) {
 | 
			
		||||
					fromElement = fromMatches[key][ secondaryIndex ];
 | 
			
		||||
					fromMatches[key][ secondaryIndex ] = null;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// If we've got a matching pair, push it to the list of pairs
 | 
			
		||||
			if( fromElement ) {
 | 
			
		||||
				pairs.push({
 | 
			
		||||
					from: fromElement,
 | 
			
		||||
					to: element,
 | 
			
		||||
					options: animationOptions
 | 
			
		||||
				});
 | 
			
		||||
			}
 | 
			
		||||
		} );
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Returns a all elements within the given scope that should
 | 
			
		||||
	 * be considered unmatched in an auto-animate transition. If
 | 
			
		||||
	 * fading of unmatched elements is turned on, these elements
 | 
			
		||||
	 * will fade when going between auto-animate slides.
 | 
			
		||||
	 *
 | 
			
		||||
	 * Note that parents of auto-animate targets are NOT considerd
 | 
			
		||||
	 * unmatched since fading them would break the auto-animation.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param {HTMLElement} rootElement
 | 
			
		||||
	 * @return {Array}
 | 
			
		||||
	 */
 | 
			
		||||
	getUnmatchedAutoAnimateElements( rootElement ) {
 | 
			
		||||
 | 
			
		||||
		return [].slice.call( rootElement.children ).reduce( ( result, element ) => {
 | 
			
		||||
 | 
			
		||||
			const containsAnimatedElements = element.querySelector( '[data-auto-animate-target]' );
 | 
			
		||||
 | 
			
		||||
			// The element is unmatched if
 | 
			
		||||
			// - It is not an auto-animate target
 | 
			
		||||
			// - It does not contain any auto-animate targets
 | 
			
		||||
			if( !element.hasAttribute( 'data-auto-animate-target' ) && !containsAnimatedElements ) {
 | 
			
		||||
				result.push( element );
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if( element.querySelector( '[data-auto-animate-target]' ) ) {
 | 
			
		||||
				result = result.concat( this.getUnmatchedAutoAnimateElements( element ) );
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			return result;
 | 
			
		||||
 | 
			
		||||
		}, [] );
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										406
									
								
								js/controllers/backgrounds.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										406
									
								
								js/controllers/backgrounds.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,406 @@
 | 
			
		||||
import { queryAll } from '../utils/util.js'
 | 
			
		||||
import { colorToRgb, colorBrightness } from '../utils/color.js'
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Creates and updates slide backgrounds.
 | 
			
		||||
 */
 | 
			
		||||
export default class Backgrounds {
 | 
			
		||||
 | 
			
		||||
	constructor( Reveal ) {
 | 
			
		||||
 | 
			
		||||
		this.Reveal = Reveal;
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render() {
 | 
			
		||||
 | 
			
		||||
		this.element = document.createElement( 'div' );
 | 
			
		||||
		this.element.className = 'backgrounds';
 | 
			
		||||
		this.Reveal.getRevealElement().appendChild( this.element );
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Creates the slide background elements and appends them
 | 
			
		||||
	 * to the background container. One element is created per
 | 
			
		||||
	 * slide no matter if the given slide has visible background.
 | 
			
		||||
	 */
 | 
			
		||||
	create() {
 | 
			
		||||
 | 
			
		||||
		// Clear prior backgrounds
 | 
			
		||||
		this.element.innerHTML = '';
 | 
			
		||||
		this.element.classList.add( 'no-transition' );
 | 
			
		||||
 | 
			
		||||
		// Iterate over all horizontal slides
 | 
			
		||||
		this.Reveal.getHorizontalSlides().forEach( slideh => {
 | 
			
		||||
 | 
			
		||||
			let backgroundStack = this.createBackground( slideh, this.element );
 | 
			
		||||
 | 
			
		||||
			// Iterate over all vertical slides
 | 
			
		||||
			queryAll( slideh, 'section' ).forEach( slidev => {
 | 
			
		||||
 | 
			
		||||
				this.createBackground( slidev, backgroundStack );
 | 
			
		||||
 | 
			
		||||
				backgroundStack.classList.add( 'stack' );
 | 
			
		||||
 | 
			
		||||
			} );
 | 
			
		||||
 | 
			
		||||
		} );
 | 
			
		||||
 | 
			
		||||
		// Add parallax background if specified
 | 
			
		||||
		if( this.Reveal.getConfig().parallaxBackgroundImage ) {
 | 
			
		||||
 | 
			
		||||
			this.element.style.backgroundImage = 'url("' + this.Reveal.getConfig().parallaxBackgroundImage + '")';
 | 
			
		||||
			this.element.style.backgroundSize = this.Reveal.getConfig().parallaxBackgroundSize;
 | 
			
		||||
			this.element.style.backgroundRepeat = this.Reveal.getConfig().parallaxBackgroundRepeat;
 | 
			
		||||
			this.element.style.backgroundPosition = this.Reveal.getConfig().parallaxBackgroundPosition;
 | 
			
		||||
 | 
			
		||||
			// Make sure the below properties are set on the element - these properties are
 | 
			
		||||
			// needed for proper transitions to be set on the element via CSS. To remove
 | 
			
		||||
			// annoying background slide-in effect when the presentation starts, apply
 | 
			
		||||
			// these properties after short time delay
 | 
			
		||||
			setTimeout( () => {
 | 
			
		||||
				this.Reveal.getRevealElement().classList.add( 'has-parallax-background' );
 | 
			
		||||
			}, 1 );
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
		else {
 | 
			
		||||
 | 
			
		||||
			this.element.style.backgroundImage = '';
 | 
			
		||||
			this.Reveal.getRevealElement().classList.remove( 'has-parallax-background' );
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Creates a background for the given slide.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param {HTMLElement} slide
 | 
			
		||||
	 * @param {HTMLElement} container The element that the background
 | 
			
		||||
	 * should be appended to
 | 
			
		||||
	 * @return {HTMLElement} New background div
 | 
			
		||||
	 */
 | 
			
		||||
	createBackground( slide, container ) {
 | 
			
		||||
 | 
			
		||||
		// Main slide background element
 | 
			
		||||
		let element = document.createElement( 'div' );
 | 
			
		||||
		element.className = 'slide-background ' + slide.className.replace( /present|past|future/, '' );
 | 
			
		||||
 | 
			
		||||
		// Inner background element that wraps images/videos/iframes
 | 
			
		||||
		let contentElement = document.createElement( 'div' );
 | 
			
		||||
		contentElement.className = 'slide-background-content';
 | 
			
		||||
 | 
			
		||||
		element.appendChild( contentElement );
 | 
			
		||||
		container.appendChild( element );
 | 
			
		||||
 | 
			
		||||
		slide.slideBackgroundElement = element;
 | 
			
		||||
		slide.slideBackgroundContentElement = contentElement;
 | 
			
		||||
 | 
			
		||||
		// Syncs the background to reflect all current background settings
 | 
			
		||||
		this.sync( slide );
 | 
			
		||||
 | 
			
		||||
		return element;
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Renders all of the visual properties of a slide background
 | 
			
		||||
	 * based on the various background attributes.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param {HTMLElement} slide
 | 
			
		||||
	 */
 | 
			
		||||
	sync( slide ) {
 | 
			
		||||
 | 
			
		||||
		const element = slide.slideBackgroundElement,
 | 
			
		||||
			contentElement = slide.slideBackgroundContentElement;
 | 
			
		||||
 | 
			
		||||
		const data = {
 | 
			
		||||
			background: slide.getAttribute( 'data-background' ),
 | 
			
		||||
			backgroundSize: slide.getAttribute( 'data-background-size' ),
 | 
			
		||||
			backgroundImage: slide.getAttribute( 'data-background-image' ),
 | 
			
		||||
			backgroundVideo: slide.getAttribute( 'data-background-video' ),
 | 
			
		||||
			backgroundIframe: slide.getAttribute( 'data-background-iframe' ),
 | 
			
		||||
			backgroundColor: slide.getAttribute( 'data-background-color' ),
 | 
			
		||||
			backgroundGradient: slide.getAttribute( 'data-background-gradient' ),
 | 
			
		||||
			backgroundRepeat: slide.getAttribute( 'data-background-repeat' ),
 | 
			
		||||
			backgroundPosition: slide.getAttribute( 'data-background-position' ),
 | 
			
		||||
			backgroundTransition: slide.getAttribute( 'data-background-transition' ),
 | 
			
		||||
			backgroundOpacity: slide.getAttribute( 'data-background-opacity' ),
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		const dataPreload = slide.hasAttribute( 'data-preload' );
 | 
			
		||||
 | 
			
		||||
		// Reset the prior background state in case this is not the
 | 
			
		||||
		// initial sync
 | 
			
		||||
		slide.classList.remove( 'has-dark-background' );
 | 
			
		||||
		slide.classList.remove( 'has-light-background' );
 | 
			
		||||
 | 
			
		||||
		element.removeAttribute( 'data-loaded' );
 | 
			
		||||
		element.removeAttribute( 'data-background-hash' );
 | 
			
		||||
		element.removeAttribute( 'data-background-size' );
 | 
			
		||||
		element.removeAttribute( 'data-background-transition' );
 | 
			
		||||
		element.style.backgroundColor = '';
 | 
			
		||||
 | 
			
		||||
		contentElement.style.backgroundSize = '';
 | 
			
		||||
		contentElement.style.backgroundRepeat = '';
 | 
			
		||||
		contentElement.style.backgroundPosition = '';
 | 
			
		||||
		contentElement.style.backgroundImage = '';
 | 
			
		||||
		contentElement.style.opacity = '';
 | 
			
		||||
		contentElement.innerHTML = '';
 | 
			
		||||
 | 
			
		||||
		if( data.background ) {
 | 
			
		||||
			// Auto-wrap image urls in url(...)
 | 
			
		||||
			if( /^(http|file|\/\/)/gi.test( data.background ) || /\.(svg|png|jpg|jpeg|gif|bmp|webp)([?#\s]|$)/gi.test( data.background ) ) {
 | 
			
		||||
				slide.setAttribute( 'data-background-image', data.background );
 | 
			
		||||
			}
 | 
			
		||||
			else {
 | 
			
		||||
				element.style.background = data.background;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Create a hash for this combination of background settings.
 | 
			
		||||
		// This is used to determine when two slide backgrounds are
 | 
			
		||||
		// the same.
 | 
			
		||||
		if( data.background || data.backgroundColor || data.backgroundGradient || data.backgroundImage || data.backgroundVideo || data.backgroundIframe ) {
 | 
			
		||||
			element.setAttribute( 'data-background-hash', data.background +
 | 
			
		||||
															data.backgroundSize +
 | 
			
		||||
															data.backgroundImage +
 | 
			
		||||
															data.backgroundVideo +
 | 
			
		||||
															data.backgroundIframe +
 | 
			
		||||
															data.backgroundColor +
 | 
			
		||||
															data.backgroundGradient +
 | 
			
		||||
															data.backgroundRepeat +
 | 
			
		||||
															data.backgroundPosition +
 | 
			
		||||
															data.backgroundTransition +
 | 
			
		||||
															data.backgroundOpacity );
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Additional and optional background properties
 | 
			
		||||
		if( data.backgroundSize ) element.setAttribute( 'data-background-size', data.backgroundSize );
 | 
			
		||||
		if( data.backgroundColor ) element.style.backgroundColor = data.backgroundColor;
 | 
			
		||||
		if( data.backgroundGradient ) element.style.backgroundImage = data.backgroundGradient;
 | 
			
		||||
		if( data.backgroundTransition ) element.setAttribute( 'data-background-transition', data.backgroundTransition );
 | 
			
		||||
 | 
			
		||||
		if( dataPreload ) element.setAttribute( 'data-preload', '' );
 | 
			
		||||
 | 
			
		||||
		// Background image options are set on the content wrapper
 | 
			
		||||
		if( data.backgroundSize ) contentElement.style.backgroundSize = data.backgroundSize;
 | 
			
		||||
		if( data.backgroundRepeat ) contentElement.style.backgroundRepeat = data.backgroundRepeat;
 | 
			
		||||
		if( data.backgroundPosition ) contentElement.style.backgroundPosition = data.backgroundPosition;
 | 
			
		||||
		if( data.backgroundOpacity ) contentElement.style.opacity = data.backgroundOpacity;
 | 
			
		||||
 | 
			
		||||
		// If this slide has a background color, we add a class that
 | 
			
		||||
		// signals if it is light or dark. If the slide has no background
 | 
			
		||||
		// color, no class will be added
 | 
			
		||||
		let contrastColor = data.backgroundColor;
 | 
			
		||||
 | 
			
		||||
		// If no bg color was found, or it cannot be converted by colorToRgb, check the computed background
 | 
			
		||||
		if( !contrastColor || !colorToRgb( contrastColor ) ) {
 | 
			
		||||
			let computedBackgroundStyle = window.getComputedStyle( element );
 | 
			
		||||
			if( computedBackgroundStyle && computedBackgroundStyle.backgroundColor ) {
 | 
			
		||||
				contrastColor = computedBackgroundStyle.backgroundColor;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if( contrastColor ) {
 | 
			
		||||
			const rgb = colorToRgb( contrastColor );
 | 
			
		||||
 | 
			
		||||
			// Ignore fully transparent backgrounds. Some browsers return
 | 
			
		||||
			// rgba(0,0,0,0) when reading the computed background color of
 | 
			
		||||
			// an element with no background
 | 
			
		||||
			if( rgb && rgb.a !== 0 ) {
 | 
			
		||||
				if( colorBrightness( contrastColor ) < 128 ) {
 | 
			
		||||
					slide.classList.add( 'has-dark-background' );
 | 
			
		||||
				}
 | 
			
		||||
				else {
 | 
			
		||||
					slide.classList.add( 'has-light-background' );
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Updates the background elements to reflect the current
 | 
			
		||||
	 * slide.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param {boolean} includeAll If true, the backgrounds of
 | 
			
		||||
	 * all vertical slides (not just the present) will be updated.
 | 
			
		||||
	 */
 | 
			
		||||
	update( includeAll = false ) {
 | 
			
		||||
 | 
			
		||||
		let currentSlide = this.Reveal.getCurrentSlide();
 | 
			
		||||
		let indices = this.Reveal.getIndices();
 | 
			
		||||
 | 
			
		||||
		let currentBackground = null;
 | 
			
		||||
 | 
			
		||||
		// Reverse past/future classes when in RTL mode
 | 
			
		||||
		let horizontalPast = this.Reveal.getConfig().rtl ? 'future' : 'past',
 | 
			
		||||
			horizontalFuture = this.Reveal.getConfig().rtl ? 'past' : 'future';
 | 
			
		||||
 | 
			
		||||
		// Update the classes of all backgrounds to match the
 | 
			
		||||
		// states of their slides (past/present/future)
 | 
			
		||||
		Array.from( this.element.childNodes ).forEach( ( backgroundh, h ) => {
 | 
			
		||||
 | 
			
		||||
			backgroundh.classList.remove( 'past', 'present', 'future' );
 | 
			
		||||
 | 
			
		||||
			if( h < indices.h ) {
 | 
			
		||||
				backgroundh.classList.add( horizontalPast );
 | 
			
		||||
			}
 | 
			
		||||
			else if ( h > indices.h ) {
 | 
			
		||||
				backgroundh.classList.add( horizontalFuture );
 | 
			
		||||
			}
 | 
			
		||||
			else {
 | 
			
		||||
				backgroundh.classList.add( 'present' );
 | 
			
		||||
 | 
			
		||||
				// Store a reference to the current background element
 | 
			
		||||
				currentBackground = backgroundh;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if( includeAll || h === indices.h ) {
 | 
			
		||||
				queryAll( backgroundh, '.slide-background' ).forEach( ( backgroundv, v ) => {
 | 
			
		||||
 | 
			
		||||
					backgroundv.classList.remove( 'past', 'present', 'future' );
 | 
			
		||||
 | 
			
		||||
					if( v < indices.v ) {
 | 
			
		||||
						backgroundv.classList.add( 'past' );
 | 
			
		||||
					}
 | 
			
		||||
					else if ( v > indices.v ) {
 | 
			
		||||
						backgroundv.classList.add( 'future' );
 | 
			
		||||
					}
 | 
			
		||||
					else {
 | 
			
		||||
						backgroundv.classList.add( 'present' );
 | 
			
		||||
 | 
			
		||||
						// Only if this is the present horizontal and vertical slide
 | 
			
		||||
						if( h === indices.h ) currentBackground = backgroundv;
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
				} );
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
		} );
 | 
			
		||||
 | 
			
		||||
		// Stop content inside of previous backgrounds
 | 
			
		||||
		if( this.previousBackground ) {
 | 
			
		||||
 | 
			
		||||
			this.Reveal.slideContent.stopEmbeddedContent( this.previousBackground, { unloadIframes: !this.Reveal.slideContent.shouldPreload( this.previousBackground ) } );
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Start content in the current background
 | 
			
		||||
		if( currentBackground ) {
 | 
			
		||||
 | 
			
		||||
			this.Reveal.slideContent.startEmbeddedContent( currentBackground );
 | 
			
		||||
 | 
			
		||||
			let currentBackgroundContent = currentBackground.querySelector( '.slide-background-content' );
 | 
			
		||||
			if( currentBackgroundContent ) {
 | 
			
		||||
 | 
			
		||||
				let backgroundImageURL = currentBackgroundContent.style.backgroundImage || '';
 | 
			
		||||
 | 
			
		||||
				// Restart GIFs (doesn't work in Firefox)
 | 
			
		||||
				if( /\.gif/i.test( backgroundImageURL ) ) {
 | 
			
		||||
					currentBackgroundContent.style.backgroundImage = '';
 | 
			
		||||
					window.getComputedStyle( currentBackgroundContent ).opacity;
 | 
			
		||||
					currentBackgroundContent.style.backgroundImage = backgroundImageURL;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// Don't transition between identical backgrounds. This
 | 
			
		||||
			// prevents unwanted flicker.
 | 
			
		||||
			let previousBackgroundHash = this.previousBackground ? this.previousBackground.getAttribute( 'data-background-hash' ) : null;
 | 
			
		||||
			let currentBackgroundHash = currentBackground.getAttribute( 'data-background-hash' );
 | 
			
		||||
			if( currentBackgroundHash && currentBackgroundHash === previousBackgroundHash && currentBackground !== this.previousBackground ) {
 | 
			
		||||
				this.element.classList.add( 'no-transition' );
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			this.previousBackground = currentBackground;
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// If there's a background brightness flag for this slide,
 | 
			
		||||
		// bubble it to the .reveal container
 | 
			
		||||
		if( currentSlide ) {
 | 
			
		||||
			[ 'has-light-background', 'has-dark-background' ].forEach( classToBubble => {
 | 
			
		||||
				if( currentSlide.classList.contains( classToBubble ) ) {
 | 
			
		||||
					this.Reveal.getRevealElement().classList.add( classToBubble );
 | 
			
		||||
				}
 | 
			
		||||
				else {
 | 
			
		||||
					this.Reveal.getRevealElement().classList.remove( classToBubble );
 | 
			
		||||
				}
 | 
			
		||||
			}, this );
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Allow the first background to apply without transition
 | 
			
		||||
		setTimeout( () => {
 | 
			
		||||
			this.element.classList.remove( 'no-transition' );
 | 
			
		||||
		}, 1 );
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Updates the position of the parallax background based
 | 
			
		||||
	 * on the current slide index.
 | 
			
		||||
	 */
 | 
			
		||||
	updateParallax() {
 | 
			
		||||
 | 
			
		||||
		let indices = this.Reveal.getIndices();
 | 
			
		||||
 | 
			
		||||
		if( this.Reveal.getConfig().parallaxBackgroundImage ) {
 | 
			
		||||
 | 
			
		||||
			let horizontalSlides = this.Reveal.getHorizontalSlides(),
 | 
			
		||||
				verticalSlides = this.Reveal.getVerticalSlides();
 | 
			
		||||
 | 
			
		||||
			let backgroundSize = this.element.style.backgroundSize.split( ' ' ),
 | 
			
		||||
				backgroundWidth, backgroundHeight;
 | 
			
		||||
 | 
			
		||||
			if( backgroundSize.length === 1 ) {
 | 
			
		||||
				backgroundWidth = backgroundHeight = parseInt( backgroundSize[0], 10 );
 | 
			
		||||
			}
 | 
			
		||||
			else {
 | 
			
		||||
				backgroundWidth = parseInt( backgroundSize[0], 10 );
 | 
			
		||||
				backgroundHeight = parseInt( backgroundSize[1], 10 );
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			let slideWidth = this.element.offsetWidth,
 | 
			
		||||
				horizontalSlideCount = horizontalSlides.length,
 | 
			
		||||
				horizontalOffsetMultiplier,
 | 
			
		||||
				horizontalOffset;
 | 
			
		||||
 | 
			
		||||
			if( typeof this.Reveal.getConfig().parallaxBackgroundHorizontal === 'number' ) {
 | 
			
		||||
				horizontalOffsetMultiplier = this.Reveal.getConfig().parallaxBackgroundHorizontal;
 | 
			
		||||
			}
 | 
			
		||||
			else {
 | 
			
		||||
				horizontalOffsetMultiplier = horizontalSlideCount > 1 ? ( backgroundWidth - slideWidth ) / ( horizontalSlideCount-1 ) : 0;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			horizontalOffset = horizontalOffsetMultiplier * indices.h * -1;
 | 
			
		||||
 | 
			
		||||
			let slideHeight = this.element.offsetHeight,
 | 
			
		||||
				verticalSlideCount = verticalSlides.length,
 | 
			
		||||
				verticalOffsetMultiplier,
 | 
			
		||||
				verticalOffset;
 | 
			
		||||
 | 
			
		||||
			if( typeof this.Reveal.getConfig().parallaxBackgroundVertical === 'number' ) {
 | 
			
		||||
				verticalOffsetMultiplier = this.Reveal.getConfig().parallaxBackgroundVertical;
 | 
			
		||||
			}
 | 
			
		||||
			else {
 | 
			
		||||
				verticalOffsetMultiplier = ( backgroundHeight - slideHeight ) / ( verticalSlideCount-1 );
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			verticalOffset = verticalSlideCount > 0 ?  verticalOffsetMultiplier * indices.v : 0;
 | 
			
		||||
 | 
			
		||||
			this.element.style.backgroundPosition = horizontalOffset + 'px ' + -verticalOffset + 'px';
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	destroy() {
 | 
			
		||||
 | 
			
		||||
		this.element.remove();
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										266
									
								
								js/controllers/controls.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										266
									
								
								js/controllers/controls.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -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();
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										103
									
								
								js/controllers/focus.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								js/controllers/focus.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,103 @@
 | 
			
		||||
import { closest } from '../utils/util.js'
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Manages focus when a presentation is embedded. This
 | 
			
		||||
 * helps us only capture keyboard from the presentation
 | 
			
		||||
 * a user is currently interacting with in a page where
 | 
			
		||||
 * multiple presentations are embedded.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
const STATE_FOCUS = 'focus';
 | 
			
		||||
const STATE_BLUR = 'blur';
 | 
			
		||||
 | 
			
		||||
export default class Focus {
 | 
			
		||||
 | 
			
		||||
	constructor( Reveal ) {
 | 
			
		||||
 | 
			
		||||
		this.Reveal = Reveal;
 | 
			
		||||
 | 
			
		||||
		this.onRevealPointerDown = this.onRevealPointerDown.bind( this );
 | 
			
		||||
		this.onDocumentPointerDown = this.onDocumentPointerDown.bind( this );
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Called when the reveal.js config is updated.
 | 
			
		||||
	 */
 | 
			
		||||
	configure( config, oldConfig ) {
 | 
			
		||||
 | 
			
		||||
		if( config.embedded ) {
 | 
			
		||||
			this.blur();
 | 
			
		||||
		}
 | 
			
		||||
		else {
 | 
			
		||||
			this.focus();
 | 
			
		||||
			this.unbind();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	bind() {
 | 
			
		||||
 | 
			
		||||
		if( this.Reveal.getConfig().embedded ) {
 | 
			
		||||
			this.Reveal.getRevealElement().addEventListener( 'pointerdown', this.onRevealPointerDown, false );
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	unbind() {
 | 
			
		||||
 | 
			
		||||
		this.Reveal.getRevealElement().removeEventListener( 'pointerdown', this.onRevealPointerDown, false );
 | 
			
		||||
		document.removeEventListener( 'pointerdown', this.onDocumentPointerDown, false );
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	focus() {
 | 
			
		||||
 | 
			
		||||
		if( this.state !== STATE_FOCUS ) {
 | 
			
		||||
			this.Reveal.getRevealElement().classList.add( 'focused' );
 | 
			
		||||
			document.addEventListener( 'pointerdown', this.onDocumentPointerDown, false );
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		this.state = STATE_FOCUS;
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	blur() {
 | 
			
		||||
 | 
			
		||||
		if( this.state !== STATE_BLUR ) {
 | 
			
		||||
			this.Reveal.getRevealElement().classList.remove( 'focused' );
 | 
			
		||||
			document.removeEventListener( 'pointerdown', this.onDocumentPointerDown, false );
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		this.state = STATE_BLUR;
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	isFocused() {
 | 
			
		||||
 | 
			
		||||
		return this.state === STATE_FOCUS;
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	destroy() {
 | 
			
		||||
 | 
			
		||||
		this.Reveal.getRevealElement().classList.remove( 'focused' );
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	onRevealPointerDown( event ) {
 | 
			
		||||
 | 
			
		||||
		this.focus();
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	onDocumentPointerDown( event ) {
 | 
			
		||||
 | 
			
		||||
		let revealElement = closest( event.target, '.reveal' );
 | 
			
		||||
		if( !revealElement || revealElement !== this.Reveal.getRevealElement() ) {
 | 
			
		||||
			this.blur();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										376
									
								
								js/controllers/fragments.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										376
									
								
								js/controllers/fragments.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,376 @@
 | 
			
		||||
import { extend, queryAll } from '../utils/util.js'
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Handles sorting and navigation of slide fragments.
 | 
			
		||||
 * Fragments are elements within a slide that are
 | 
			
		||||
 * revealed/animated incrementally.
 | 
			
		||||
 */
 | 
			
		||||
export default class Fragments {
 | 
			
		||||
 | 
			
		||||
	constructor( Reveal ) {
 | 
			
		||||
 | 
			
		||||
		this.Reveal = Reveal;
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Called when the reveal.js config is updated.
 | 
			
		||||
	 */
 | 
			
		||||
	configure( config, oldConfig ) {
 | 
			
		||||
 | 
			
		||||
		if( config.fragments === false ) {
 | 
			
		||||
			this.disable();
 | 
			
		||||
		}
 | 
			
		||||
		else if( oldConfig.fragments === false ) {
 | 
			
		||||
			this.enable();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * If fragments are disabled in the deck, they should all be
 | 
			
		||||
	 * visible rather than stepped through.
 | 
			
		||||
	 */
 | 
			
		||||
	disable() {
 | 
			
		||||
 | 
			
		||||
		queryAll( this.Reveal.getSlidesElement(), '.fragment' ).forEach( element => {
 | 
			
		||||
			element.classList.add( 'visible' );
 | 
			
		||||
			element.classList.remove( 'current-fragment' );
 | 
			
		||||
		} );
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Reverse of #disable(). Only called if fragments have
 | 
			
		||||
	 * previously been disabled.
 | 
			
		||||
	 */
 | 
			
		||||
	enable() {
 | 
			
		||||
 | 
			
		||||
		queryAll( this.Reveal.getSlidesElement(), '.fragment' ).forEach( element => {
 | 
			
		||||
			element.classList.remove( 'visible' );
 | 
			
		||||
			element.classList.remove( 'current-fragment' );
 | 
			
		||||
		} );
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Returns an object describing the available fragment
 | 
			
		||||
	 * directions.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @return {{prev: boolean, next: boolean}}
 | 
			
		||||
	 */
 | 
			
		||||
	availableRoutes() {
 | 
			
		||||
 | 
			
		||||
		let currentSlide = this.Reveal.getCurrentSlide();
 | 
			
		||||
		if( currentSlide && this.Reveal.getConfig().fragments ) {
 | 
			
		||||
			let fragments = currentSlide.querySelectorAll( '.fragment:not(.disabled)' );
 | 
			
		||||
			let hiddenFragments = currentSlide.querySelectorAll( '.fragment:not(.disabled):not(.visible)' );
 | 
			
		||||
 | 
			
		||||
			return {
 | 
			
		||||
				prev: fragments.length - hiddenFragments.length > 0,
 | 
			
		||||
				next: !!hiddenFragments.length
 | 
			
		||||
			};
 | 
			
		||||
		}
 | 
			
		||||
		else {
 | 
			
		||||
			return { prev: false, next: false };
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Return a sorted fragments list, ordered by an increasing
 | 
			
		||||
	 * "data-fragment-index" attribute.
 | 
			
		||||
	 *
 | 
			
		||||
	 * Fragments will be revealed in the order that they are returned by
 | 
			
		||||
	 * this function, so you can use the index attributes to control the
 | 
			
		||||
	 * order of fragment appearance.
 | 
			
		||||
	 *
 | 
			
		||||
	 * To maintain a sensible default fragment order, fragments are presumed
 | 
			
		||||
	 * to be passed in document order. This function adds a "fragment-index"
 | 
			
		||||
	 * attribute to each node if such an attribute is not already present,
 | 
			
		||||
	 * and sets that attribute to an integer value which is the position of
 | 
			
		||||
	 * the fragment within the fragments list.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param {object[]|*} fragments
 | 
			
		||||
	 * @param {boolean} grouped If true the returned array will contain
 | 
			
		||||
	 * nested arrays for all fragments with the same index
 | 
			
		||||
	 * @return {object[]} sorted Sorted array of fragments
 | 
			
		||||
	 */
 | 
			
		||||
	sort( fragments, grouped = false ) {
 | 
			
		||||
 | 
			
		||||
		fragments = Array.from( fragments );
 | 
			
		||||
 | 
			
		||||
		let ordered = [],
 | 
			
		||||
			unordered = [],
 | 
			
		||||
			sorted = [];
 | 
			
		||||
 | 
			
		||||
		// Group ordered and unordered elements
 | 
			
		||||
		fragments.forEach( fragment => {
 | 
			
		||||
			if( fragment.hasAttribute( 'data-fragment-index' ) ) {
 | 
			
		||||
				let index = parseInt( fragment.getAttribute( 'data-fragment-index' ), 10 );
 | 
			
		||||
 | 
			
		||||
				if( !ordered[index] ) {
 | 
			
		||||
					ordered[index] = [];
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				ordered[index].push( fragment );
 | 
			
		||||
			}
 | 
			
		||||
			else {
 | 
			
		||||
				unordered.push( [ fragment ] );
 | 
			
		||||
			}
 | 
			
		||||
		} );
 | 
			
		||||
 | 
			
		||||
		// Append fragments without explicit indices in their
 | 
			
		||||
		// DOM order
 | 
			
		||||
		ordered = ordered.concat( unordered );
 | 
			
		||||
 | 
			
		||||
		// Manually count the index up per group to ensure there
 | 
			
		||||
		// are no gaps
 | 
			
		||||
		let index = 0;
 | 
			
		||||
 | 
			
		||||
		// Push all fragments in their sorted order to an array,
 | 
			
		||||
		// this flattens the groups
 | 
			
		||||
		ordered.forEach( group => {
 | 
			
		||||
			group.forEach( fragment => {
 | 
			
		||||
				sorted.push( fragment );
 | 
			
		||||
				fragment.setAttribute( 'data-fragment-index', index );
 | 
			
		||||
			} );
 | 
			
		||||
 | 
			
		||||
			index ++;
 | 
			
		||||
		} );
 | 
			
		||||
 | 
			
		||||
		return grouped === true ? ordered : sorted;
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Sorts and formats all of fragments in the
 | 
			
		||||
	 * presentation.
 | 
			
		||||
	 */
 | 
			
		||||
	sortAll() {
 | 
			
		||||
 | 
			
		||||
		this.Reveal.getHorizontalSlides().forEach( horizontalSlide => {
 | 
			
		||||
 | 
			
		||||
			let verticalSlides = queryAll( horizontalSlide, 'section' );
 | 
			
		||||
			verticalSlides.forEach( ( verticalSlide, y ) => {
 | 
			
		||||
 | 
			
		||||
				this.sort( verticalSlide.querySelectorAll( '.fragment' ) );
 | 
			
		||||
 | 
			
		||||
			}, this );
 | 
			
		||||
 | 
			
		||||
			if( verticalSlides.length === 0 ) this.sort( horizontalSlide.querySelectorAll( '.fragment' ) );
 | 
			
		||||
 | 
			
		||||
		} );
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Refreshes the fragments on the current slide so that they
 | 
			
		||||
	 * have the appropriate classes (.visible + .current-fragment).
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param {number} [index] The index of the current fragment
 | 
			
		||||
	 * @param {array} [fragments] Array containing all fragments
 | 
			
		||||
	 * in the current slide
 | 
			
		||||
	 *
 | 
			
		||||
	 * @return {{shown: array, hidden: array}}
 | 
			
		||||
	 */
 | 
			
		||||
	update( index, fragments ) {
 | 
			
		||||
 | 
			
		||||
		let changedFragments = {
 | 
			
		||||
			shown: [],
 | 
			
		||||
			hidden: []
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		let currentSlide = this.Reveal.getCurrentSlide();
 | 
			
		||||
		if( currentSlide && this.Reveal.getConfig().fragments ) {
 | 
			
		||||
 | 
			
		||||
			fragments = fragments || this.sort( currentSlide.querySelectorAll( '.fragment' ) );
 | 
			
		||||
 | 
			
		||||
			if( fragments.length ) {
 | 
			
		||||
 | 
			
		||||
				let maxIndex = 0;
 | 
			
		||||
 | 
			
		||||
				if( typeof index !== 'number' ) {
 | 
			
		||||
					let currentFragment = this.sort( currentSlide.querySelectorAll( '.fragment.visible' ) ).pop();
 | 
			
		||||
					if( currentFragment ) {
 | 
			
		||||
						index = parseInt( currentFragment.getAttribute( 'data-fragment-index' ) || 0, 10 );
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				Array.from( fragments ).forEach( ( el, i ) => {
 | 
			
		||||
 | 
			
		||||
					if( el.hasAttribute( 'data-fragment-index' ) ) {
 | 
			
		||||
						i = parseInt( el.getAttribute( 'data-fragment-index' ), 10 );
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					maxIndex = Math.max( maxIndex, i );
 | 
			
		||||
 | 
			
		||||
					// Visible fragments
 | 
			
		||||
					if( i <= index ) {
 | 
			
		||||
						let wasVisible = el.classList.contains( 'visible' )
 | 
			
		||||
						el.classList.add( 'visible' );
 | 
			
		||||
						el.classList.remove( 'current-fragment' );
 | 
			
		||||
 | 
			
		||||
						if( i === index ) {
 | 
			
		||||
							// Announce the fragments one by one to the Screen Reader
 | 
			
		||||
							this.Reveal.announceStatus( this.Reveal.getStatusText( el ) );
 | 
			
		||||
 | 
			
		||||
							el.classList.add( 'current-fragment' );
 | 
			
		||||
							this.Reveal.slideContent.startEmbeddedContent( el );
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						if( !wasVisible ) {
 | 
			
		||||
							changedFragments.shown.push( el )
 | 
			
		||||
							this.Reveal.dispatchEvent({
 | 
			
		||||
								target: el,
 | 
			
		||||
								type: 'visible',
 | 
			
		||||
								bubbles: false
 | 
			
		||||
							});
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
					// Hidden fragments
 | 
			
		||||
					else {
 | 
			
		||||
						let wasVisible = el.classList.contains( 'visible' )
 | 
			
		||||
						el.classList.remove( 'visible' );
 | 
			
		||||
						el.classList.remove( 'current-fragment' );
 | 
			
		||||
 | 
			
		||||
						if( wasVisible ) {
 | 
			
		||||
							this.Reveal.slideContent.stopEmbeddedContent( el );
 | 
			
		||||
							changedFragments.hidden.push( el );
 | 
			
		||||
							this.Reveal.dispatchEvent({
 | 
			
		||||
								target: el,
 | 
			
		||||
								type: 'hidden',
 | 
			
		||||
								bubbles: false
 | 
			
		||||
							});
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
				} );
 | 
			
		||||
 | 
			
		||||
				// Write the current fragment index to the slide <section>.
 | 
			
		||||
				// This can be used by end users to apply styles based on
 | 
			
		||||
				// the current fragment index.
 | 
			
		||||
				index = typeof index === 'number' ? index : -1;
 | 
			
		||||
				index = Math.max( Math.min( index, maxIndex ), -1 );
 | 
			
		||||
				currentSlide.setAttribute( 'data-fragment', index );
 | 
			
		||||
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return changedFragments;
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Formats the fragments on the given slide so that they have
 | 
			
		||||
	 * valid indices. Call this if fragments are changed in the DOM
 | 
			
		||||
	 * after reveal.js has already initialized.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param {HTMLElement} slide
 | 
			
		||||
	 * @return {Array} a list of the HTML fragments that were synced
 | 
			
		||||
	 */
 | 
			
		||||
	sync( slide = this.Reveal.getCurrentSlide() ) {
 | 
			
		||||
 | 
			
		||||
		return this.sort( slide.querySelectorAll( '.fragment' ) );
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Navigate to the specified slide fragment.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param {?number} index The index of the fragment that
 | 
			
		||||
	 * should be shown, -1 means all are invisible
 | 
			
		||||
	 * @param {number} offset Integer offset to apply to the
 | 
			
		||||
	 * fragment index
 | 
			
		||||
	 *
 | 
			
		||||
	 * @return {boolean} true if a change was made in any
 | 
			
		||||
	 * fragments visibility as part of this call
 | 
			
		||||
	 */
 | 
			
		||||
	goto( index, offset = 0 ) {
 | 
			
		||||
 | 
			
		||||
		let currentSlide = this.Reveal.getCurrentSlide();
 | 
			
		||||
		if( currentSlide && this.Reveal.getConfig().fragments ) {
 | 
			
		||||
 | 
			
		||||
			let fragments = this.sort( currentSlide.querySelectorAll( '.fragment:not(.disabled)' ) );
 | 
			
		||||
			if( fragments.length ) {
 | 
			
		||||
 | 
			
		||||
				// If no index is specified, find the current
 | 
			
		||||
				if( typeof index !== 'number' ) {
 | 
			
		||||
					let lastVisibleFragment = this.sort( currentSlide.querySelectorAll( '.fragment:not(.disabled).visible' ) ).pop();
 | 
			
		||||
 | 
			
		||||
					if( lastVisibleFragment ) {
 | 
			
		||||
						index = parseInt( lastVisibleFragment.getAttribute( 'data-fragment-index' ) || 0, 10 );
 | 
			
		||||
					}
 | 
			
		||||
					else {
 | 
			
		||||
						index = -1;
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				// Apply the offset if there is one
 | 
			
		||||
				index += offset;
 | 
			
		||||
 | 
			
		||||
				let changedFragments = this.update( index, fragments );
 | 
			
		||||
 | 
			
		||||
				if( changedFragments.hidden.length ) {
 | 
			
		||||
					this.Reveal.dispatchEvent({
 | 
			
		||||
						type: 'fragmenthidden',
 | 
			
		||||
						data: {
 | 
			
		||||
							fragment: changedFragments.hidden[0],
 | 
			
		||||
							fragments: changedFragments.hidden
 | 
			
		||||
						}
 | 
			
		||||
					});
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				if( changedFragments.shown.length ) {
 | 
			
		||||
					this.Reveal.dispatchEvent({
 | 
			
		||||
						type: 'fragmentshown',
 | 
			
		||||
						data: {
 | 
			
		||||
							fragment: changedFragments.shown[0],
 | 
			
		||||
							fragments: changedFragments.shown
 | 
			
		||||
						}
 | 
			
		||||
					});
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				this.Reveal.controls.update();
 | 
			
		||||
				this.Reveal.progress.update();
 | 
			
		||||
 | 
			
		||||
				if( this.Reveal.getConfig().fragmentInURL ) {
 | 
			
		||||
					this.Reveal.location.writeURL();
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				return !!( changedFragments.shown.length || changedFragments.hidden.length );
 | 
			
		||||
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return false;
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Navigate to the next slide fragment.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @return {boolean} true if there was a next fragment,
 | 
			
		||||
	 * false otherwise
 | 
			
		||||
	 */
 | 
			
		||||
	next() {
 | 
			
		||||
 | 
			
		||||
		return this.goto( null, 1 );
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Navigate to the previous slide fragment.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @return {boolean} true if there was a previous fragment,
 | 
			
		||||
	 * false otherwise
 | 
			
		||||
	 */
 | 
			
		||||
	prev() {
 | 
			
		||||
 | 
			
		||||
		return this.goto( null, -1 );
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										392
									
								
								js/controllers/keyboard.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										392
									
								
								js/controllers/keyboard.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,392 @@
 | 
			
		||||
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['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 );
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			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();
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										245
									
								
								js/controllers/location.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										245
									
								
								js/controllers/location.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,245 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Reads and writes the URL based on reveal.js' current state.
 | 
			
		||||
 */
 | 
			
		||||
export default class Location {
 | 
			
		||||
 | 
			
		||||
	// The minimum number of milliseconds that must pass between
 | 
			
		||||
	// calls to history.replaceState
 | 
			
		||||
	MAX_REPLACE_STATE_FREQUENCY = 1000
 | 
			
		||||
 | 
			
		||||
	constructor( Reveal ) {
 | 
			
		||||
 | 
			
		||||
		this.Reveal = Reveal;
 | 
			
		||||
 | 
			
		||||
		// Delays updates to the URL due to a Chrome thumbnailer bug
 | 
			
		||||
		this.writeURLTimeout = 0;
 | 
			
		||||
 | 
			
		||||
		this.replaceStateTimestamp = 0;
 | 
			
		||||
 | 
			
		||||
		this.onWindowHashChange = this.onWindowHashChange.bind( this );
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	bind() {
 | 
			
		||||
 | 
			
		||||
		window.addEventListener( 'hashchange', this.onWindowHashChange, false );
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	unbind() {
 | 
			
		||||
 | 
			
		||||
		window.removeEventListener( 'hashchange', this.onWindowHashChange, false );
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Returns the slide indices for the given hash link.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param {string} [hash] the hash string that we want to
 | 
			
		||||
	 * find the indices for
 | 
			
		||||
	 *
 | 
			
		||||
	 * @returns slide indices or null
 | 
			
		||||
	 */
 | 
			
		||||
	getIndicesFromHash( hash=window.location.hash ) {
 | 
			
		||||
 | 
			
		||||
		// Attempt to parse the hash as either an index or name
 | 
			
		||||
		let name = hash.replace( /^#\/?/, '' );
 | 
			
		||||
		let bits = name.split( '/' );
 | 
			
		||||
 | 
			
		||||
		// If the first bit is not fully numeric and there is a name we
 | 
			
		||||
		// can assume that this is a named link
 | 
			
		||||
		if( !/^[0-9]*$/.test( bits[0] ) && name.length ) {
 | 
			
		||||
			let element;
 | 
			
		||||
 | 
			
		||||
			let f;
 | 
			
		||||
 | 
			
		||||
			// Parse named links with fragments (#/named-link/2)
 | 
			
		||||
			if( /\/[-\d]+$/g.test( name ) ) {
 | 
			
		||||
				f = parseInt( name.split( '/' ).pop(), 10 );
 | 
			
		||||
				f = isNaN(f) ? undefined : f;
 | 
			
		||||
				name = name.split( '/' ).shift();
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// Ensure the named link is a valid HTML ID attribute
 | 
			
		||||
			try {
 | 
			
		||||
				element = document.getElementById( decodeURIComponent( name ) );
 | 
			
		||||
			}
 | 
			
		||||
			catch ( error ) { }
 | 
			
		||||
 | 
			
		||||
			if( element ) {
 | 
			
		||||
				return { ...this.Reveal.getIndices( element ), f };
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		else {
 | 
			
		||||
			const config = this.Reveal.getConfig();
 | 
			
		||||
			let hashIndexBase = config.hashOneBasedIndex ? 1 : 0;
 | 
			
		||||
 | 
			
		||||
			// Read the index components of the hash
 | 
			
		||||
			let h = ( parseInt( bits[0], 10 ) - hashIndexBase ) || 0,
 | 
			
		||||
				v = ( parseInt( bits[1], 10 ) - hashIndexBase ) || 0,
 | 
			
		||||
				f;
 | 
			
		||||
 | 
			
		||||
			if( config.fragmentInURL ) {
 | 
			
		||||
				f = parseInt( bits[2], 10 );
 | 
			
		||||
				if( isNaN( f ) ) {
 | 
			
		||||
					f = undefined;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			return { h, v, f };
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// The hash couldn't be parsed or no matching named link was found
 | 
			
		||||
		return null
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Reads the current URL (hash) and navigates accordingly.
 | 
			
		||||
	 */
 | 
			
		||||
	readURL() {
 | 
			
		||||
 | 
			
		||||
		const currentIndices = this.Reveal.getIndices();
 | 
			
		||||
		const newIndices = this.getIndicesFromHash();
 | 
			
		||||
 | 
			
		||||
		if( newIndices ) {
 | 
			
		||||
			if( ( newIndices.h !== currentIndices.h || newIndices.v !== currentIndices.v || newIndices.f !== undefined ) ) {
 | 
			
		||||
					this.Reveal.slide( newIndices.h, newIndices.v, newIndices.f );
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		// If no new indices are available, we're trying to navigate to
 | 
			
		||||
		// a slide hash that does not exist
 | 
			
		||||
		else {
 | 
			
		||||
			this.Reveal.slide( currentIndices.h || 0, currentIndices.v || 0 );
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Updates the page URL (hash) to reflect the current
 | 
			
		||||
	 * state.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param {number} delay The time in ms to wait before
 | 
			
		||||
	 * writing the hash
 | 
			
		||||
	 */
 | 
			
		||||
	writeURL( delay ) {
 | 
			
		||||
 | 
			
		||||
		let config = this.Reveal.getConfig();
 | 
			
		||||
		let currentSlide = this.Reveal.getCurrentSlide();
 | 
			
		||||
 | 
			
		||||
		// Make sure there's never more than one timeout running
 | 
			
		||||
		clearTimeout( this.writeURLTimeout );
 | 
			
		||||
 | 
			
		||||
		// If a delay is specified, timeout this call
 | 
			
		||||
		if( typeof delay === 'number' ) {
 | 
			
		||||
			this.writeURLTimeout = setTimeout( this.writeURL, delay );
 | 
			
		||||
		}
 | 
			
		||||
		else if( currentSlide ) {
 | 
			
		||||
 | 
			
		||||
			let hash = this.getHash();
 | 
			
		||||
 | 
			
		||||
			// If we're configured to push to history OR the history
 | 
			
		||||
			// API is not avaialble.
 | 
			
		||||
			if( config.history ) {
 | 
			
		||||
				window.location.hash = hash;
 | 
			
		||||
			}
 | 
			
		||||
			// If we're configured to reflect the current slide in the
 | 
			
		||||
			// URL without pushing to history.
 | 
			
		||||
			else if( config.hash ) {
 | 
			
		||||
				// If the hash is empty, don't add it to the URL
 | 
			
		||||
				if( hash === '/' ) {
 | 
			
		||||
					this.debouncedReplaceState( window.location.pathname + window.location.search );
 | 
			
		||||
				}
 | 
			
		||||
				else {
 | 
			
		||||
					this.debouncedReplaceState( '#' + hash );
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			// UPDATE: The below nuking of all hash changes breaks
 | 
			
		||||
			// anchors on pages where reveal.js is running. Removed
 | 
			
		||||
			// in 4.0. Why was it here in the first place? ¯\_(ツ)_/¯
 | 
			
		||||
			//
 | 
			
		||||
			// If history and hash are both disabled, a hash may still
 | 
			
		||||
			// be added to the URL by clicking on a href with a hash
 | 
			
		||||
			// target. Counter this by always removing the hash.
 | 
			
		||||
			// else {
 | 
			
		||||
			// 	window.history.replaceState( null, null, window.location.pathname + window.location.search );
 | 
			
		||||
			// }
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	replaceState( url ) {
 | 
			
		||||
 | 
			
		||||
		window.history.replaceState( null, null, url );
 | 
			
		||||
		this.replaceStateTimestamp = Date.now();
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	debouncedReplaceState( url ) {
 | 
			
		||||
 | 
			
		||||
		clearTimeout( this.replaceStateTimeout );
 | 
			
		||||
 | 
			
		||||
		if( Date.now() - this.replaceStateTimestamp > this.MAX_REPLACE_STATE_FREQUENCY ) {
 | 
			
		||||
			this.replaceState( url );
 | 
			
		||||
		}
 | 
			
		||||
		else {
 | 
			
		||||
			this.replaceStateTimeout = setTimeout( () => this.replaceState( url ), this.MAX_REPLACE_STATE_FREQUENCY );
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Return a hash URL that will resolve to the given slide location.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param {HTMLElement} [slide=currentSlide] The slide to link to
 | 
			
		||||
	 */
 | 
			
		||||
	getHash( slide ) {
 | 
			
		||||
 | 
			
		||||
		let url = '/';
 | 
			
		||||
 | 
			
		||||
		// Attempt to create a named link based on the slide's ID
 | 
			
		||||
		let s = slide || this.Reveal.getCurrentSlide();
 | 
			
		||||
		let id = s ? s.getAttribute( 'id' ) : null;
 | 
			
		||||
		if( id ) {
 | 
			
		||||
			id = encodeURIComponent( id );
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		let index = this.Reveal.getIndices( slide );
 | 
			
		||||
		if( !this.Reveal.getConfig().fragmentInURL ) {
 | 
			
		||||
			index.f = undefined;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// If the current slide has an ID, use that as a named link,
 | 
			
		||||
		// but we don't support named links with a fragment index
 | 
			
		||||
		if( typeof id === 'string' && id.length ) {
 | 
			
		||||
			url = '/' + id;
 | 
			
		||||
 | 
			
		||||
			// If there is also a fragment, append that at the end
 | 
			
		||||
			// of the named link, like: #/named-link/2
 | 
			
		||||
			if( index.f >= 0 ) url += '/' + index.f;
 | 
			
		||||
		}
 | 
			
		||||
		// Otherwise use the /h/v index
 | 
			
		||||
		else {
 | 
			
		||||
			let hashIndexBase = this.Reveal.getConfig().hashOneBasedIndex ? 1 : 0;
 | 
			
		||||
			if( index.h > 0 || index.v > 0 || index.f >= 0 ) url += index.h + hashIndexBase;
 | 
			
		||||
			if( index.v > 0 || index.f >= 0 ) url += '/' + (index.v + hashIndexBase );
 | 
			
		||||
			if( index.f >= 0 ) url += '/' + index.f;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return url;
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Handler for the window level 'hashchange' event.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param {object} [event]
 | 
			
		||||
	 */
 | 
			
		||||
	onWindowHashChange( event ) {
 | 
			
		||||
 | 
			
		||||
		this.readURL();
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										120
									
								
								js/controllers/notes.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								js/controllers/notes.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,120 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Handles the showing and 
 | 
			
		||||
 */
 | 
			
		||||
export default class Notes {
 | 
			
		||||
 | 
			
		||||
	constructor( Reveal ) {
 | 
			
		||||
 | 
			
		||||
		this.Reveal = Reveal;
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render() {
 | 
			
		||||
 | 
			
		||||
		this.element = document.createElement( 'div' );
 | 
			
		||||
		this.element.className = 'speaker-notes';
 | 
			
		||||
		this.element.setAttribute( 'data-prevent-swipe', '' );
 | 
			
		||||
		this.element.setAttribute( 'tabindex', '0' );
 | 
			
		||||
		this.Reveal.getRevealElement().appendChild( this.element );
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Called when the reveal.js config is updated.
 | 
			
		||||
	 */
 | 
			
		||||
	configure( config, oldConfig ) {
 | 
			
		||||
 | 
			
		||||
		if( config.showNotes ) {
 | 
			
		||||
			this.element.setAttribute( 'data-layout', typeof config.showNotes === 'string' ? config.showNotes : 'inline' );
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Pick up notes from the current slide and display them
 | 
			
		||||
	 * to the viewer.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @see {@link config.showNotes}
 | 
			
		||||
	 */
 | 
			
		||||
	update() {
 | 
			
		||||
 | 
			
		||||
		if( this.Reveal.getConfig().showNotes && this.element && this.Reveal.getCurrentSlide() && !this.Reveal.print.isPrintingPDF() ) {
 | 
			
		||||
 | 
			
		||||
			this.element.innerHTML = this.getSlideNotes() || '<span class="notes-placeholder">No notes on this slide.</span>';
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Updates the visibility of the speaker notes sidebar that
 | 
			
		||||
	 * is used to share annotated slides. The notes sidebar is
 | 
			
		||||
	 * only visible if showNotes is true and there are notes on
 | 
			
		||||
	 * one or more slides in the deck.
 | 
			
		||||
	 */
 | 
			
		||||
	updateVisibility() {
 | 
			
		||||
 | 
			
		||||
		if( this.Reveal.getConfig().showNotes && this.hasNotes() && !this.Reveal.print.isPrintingPDF() ) {
 | 
			
		||||
			this.Reveal.getRevealElement().classList.add( 'show-notes' );
 | 
			
		||||
		}
 | 
			
		||||
		else {
 | 
			
		||||
			this.Reveal.getRevealElement().classList.remove( 'show-notes' );
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Checks if there are speaker notes for ANY slide in the
 | 
			
		||||
	 * presentation.
 | 
			
		||||
	 */
 | 
			
		||||
	hasNotes() {
 | 
			
		||||
 | 
			
		||||
		return this.Reveal.getSlidesElement().querySelectorAll( '[data-notes], aside.notes' ).length > 0;
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Checks if this presentation is running inside of the
 | 
			
		||||
	 * speaker notes window.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @return {boolean}
 | 
			
		||||
	 */
 | 
			
		||||
	isSpeakerNotesWindow() {
 | 
			
		||||
 | 
			
		||||
		return !!window.location.search.match( /receiver/gi );
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Retrieves the speaker notes from a slide. Notes can be
 | 
			
		||||
	 * defined in two ways:
 | 
			
		||||
	 * 1. As a data-notes attribute on the slide <section>
 | 
			
		||||
	 * 2. As an <aside class="notes"> inside of the slide
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param {HTMLElement} [slide=currentSlide]
 | 
			
		||||
	 * @return {(string|null)}
 | 
			
		||||
	 */
 | 
			
		||||
	getSlideNotes( slide = this.Reveal.getCurrentSlide() ) {
 | 
			
		||||
 | 
			
		||||
		// Notes can be specified via the data-notes attribute...
 | 
			
		||||
		if( slide.hasAttribute( 'data-notes' ) ) {
 | 
			
		||||
			return slide.getAttribute( 'data-notes' );
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// ... or using an <aside class="notes"> element
 | 
			
		||||
		let notesElement = slide.querySelector( 'aside.notes' );
 | 
			
		||||
		if( notesElement ) {
 | 
			
		||||
			return notesElement.innerHTML;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return null;
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	destroy() {
 | 
			
		||||
 | 
			
		||||
		this.element.remove();
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										255
									
								
								js/controllers/overview.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										255
									
								
								js/controllers/overview.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,255 @@
 | 
			
		||||
import { SLIDES_SELECTOR } from '../utils/constants.js'
 | 
			
		||||
import { extend, queryAll, transformElement } from '../utils/util.js'
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Handles all logic related to the overview mode
 | 
			
		||||
 * (birds-eye view of all slides).
 | 
			
		||||
 */
 | 
			
		||||
export default class Overview {
 | 
			
		||||
 | 
			
		||||
	constructor( Reveal ) {
 | 
			
		||||
 | 
			
		||||
		this.Reveal = Reveal;
 | 
			
		||||
 | 
			
		||||
		this.active = false;
 | 
			
		||||
 | 
			
		||||
		this.onSlideClicked = this.onSlideClicked.bind( this );
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Displays the overview of slides (quick nav) by scaling
 | 
			
		||||
	 * down and arranging all slide elements.
 | 
			
		||||
	 */
 | 
			
		||||
	activate() {
 | 
			
		||||
 | 
			
		||||
		// Only proceed if enabled in config
 | 
			
		||||
		if( this.Reveal.getConfig().overview && !this.isActive() ) {
 | 
			
		||||
 | 
			
		||||
			this.active = true;
 | 
			
		||||
 | 
			
		||||
			this.Reveal.getRevealElement().classList.add( 'overview' );
 | 
			
		||||
 | 
			
		||||
			// Don't auto-slide while in overview mode
 | 
			
		||||
			this.Reveal.cancelAutoSlide();
 | 
			
		||||
 | 
			
		||||
			// Move the backgrounds element into the slide container to
 | 
			
		||||
			// that the same scaling is applied
 | 
			
		||||
			this.Reveal.getSlidesElement().appendChild( this.Reveal.getBackgroundsElement() );
 | 
			
		||||
 | 
			
		||||
			// Clicking on an overview slide navigates to it
 | 
			
		||||
			queryAll( this.Reveal.getRevealElement(), SLIDES_SELECTOR ).forEach( slide => {
 | 
			
		||||
				if( !slide.classList.contains( 'stack' ) ) {
 | 
			
		||||
					slide.addEventListener( 'click', this.onSlideClicked, true );
 | 
			
		||||
				}
 | 
			
		||||
			} );
 | 
			
		||||
 | 
			
		||||
			// Calculate slide sizes
 | 
			
		||||
			const margin = 70;
 | 
			
		||||
			const slideSize = this.Reveal.getComputedSlideSize();
 | 
			
		||||
			this.overviewSlideWidth = slideSize.width + margin;
 | 
			
		||||
			this.overviewSlideHeight = slideSize.height + margin;
 | 
			
		||||
 | 
			
		||||
			// Reverse in RTL mode
 | 
			
		||||
			if( this.Reveal.getConfig().rtl ) {
 | 
			
		||||
				this.overviewSlideWidth = -this.overviewSlideWidth;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			this.Reveal.updateSlidesVisibility();
 | 
			
		||||
 | 
			
		||||
			this.layout();
 | 
			
		||||
			this.update();
 | 
			
		||||
 | 
			
		||||
			this.Reveal.layout();
 | 
			
		||||
 | 
			
		||||
			const indices = this.Reveal.getIndices();
 | 
			
		||||
 | 
			
		||||
			// Notify observers of the overview showing
 | 
			
		||||
			this.Reveal.dispatchEvent({
 | 
			
		||||
				type: 'overviewshown',
 | 
			
		||||
				data: {
 | 
			
		||||
					'indexh': indices.h,
 | 
			
		||||
					'indexv': indices.v,
 | 
			
		||||
					'currentSlide': this.Reveal.getCurrentSlide()
 | 
			
		||||
				}
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Uses CSS transforms to position all slides in a grid for
 | 
			
		||||
	 * display inside of the overview mode.
 | 
			
		||||
	 */
 | 
			
		||||
	layout() {
 | 
			
		||||
 | 
			
		||||
		// Layout slides
 | 
			
		||||
		this.Reveal.getHorizontalSlides().forEach( ( hslide, h ) => {
 | 
			
		||||
			hslide.setAttribute( 'data-index-h', h );
 | 
			
		||||
			transformElement( hslide, 'translate3d(' + ( h * this.overviewSlideWidth ) + 'px, 0, 0)' );
 | 
			
		||||
 | 
			
		||||
			if( hslide.classList.contains( 'stack' ) ) {
 | 
			
		||||
 | 
			
		||||
				queryAll( hslide, 'section' ).forEach( ( vslide, v ) => {
 | 
			
		||||
					vslide.setAttribute( 'data-index-h', h );
 | 
			
		||||
					vslide.setAttribute( 'data-index-v', v );
 | 
			
		||||
 | 
			
		||||
					transformElement( vslide, 'translate3d(0, ' + ( v * this.overviewSlideHeight ) + 'px, 0)' );
 | 
			
		||||
				} );
 | 
			
		||||
 | 
			
		||||
			}
 | 
			
		||||
		} );
 | 
			
		||||
 | 
			
		||||
		// Layout slide backgrounds
 | 
			
		||||
		Array.from( this.Reveal.getBackgroundsElement().childNodes ).forEach( ( hbackground, h ) => {
 | 
			
		||||
			transformElement( hbackground, 'translate3d(' + ( h * this.overviewSlideWidth ) + 'px, 0, 0)' );
 | 
			
		||||
 | 
			
		||||
			queryAll( hbackground, '.slide-background' ).forEach( ( vbackground, v ) => {
 | 
			
		||||
				transformElement( vbackground, 'translate3d(0, ' + ( v * this.overviewSlideHeight ) + 'px, 0)' );
 | 
			
		||||
			} );
 | 
			
		||||
		} );
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Moves the overview viewport to the current slides.
 | 
			
		||||
	 * Called each time the current slide changes.
 | 
			
		||||
	 */
 | 
			
		||||
	update() {
 | 
			
		||||
 | 
			
		||||
		const vmin = Math.min( window.innerWidth, window.innerHeight );
 | 
			
		||||
		const scale = Math.max( vmin / 5, 150 ) / vmin;
 | 
			
		||||
		const indices = this.Reveal.getIndices();
 | 
			
		||||
 | 
			
		||||
		this.Reveal.transformSlides( {
 | 
			
		||||
			overview: [
 | 
			
		||||
				'scale('+ scale +')',
 | 
			
		||||
				'translateX('+ ( -indices.h * this.overviewSlideWidth ) +'px)',
 | 
			
		||||
				'translateY('+ ( -indices.v * this.overviewSlideHeight ) +'px)'
 | 
			
		||||
			].join( ' ' )
 | 
			
		||||
		} );
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Exits the slide overview and enters the currently
 | 
			
		||||
	 * active slide.
 | 
			
		||||
	 */
 | 
			
		||||
	deactivate() {
 | 
			
		||||
 | 
			
		||||
		// Only proceed if enabled in config
 | 
			
		||||
		if( this.Reveal.getConfig().overview ) {
 | 
			
		||||
 | 
			
		||||
			this.active = false;
 | 
			
		||||
 | 
			
		||||
			this.Reveal.getRevealElement().classList.remove( 'overview' );
 | 
			
		||||
 | 
			
		||||
			// Temporarily add a class so that transitions can do different things
 | 
			
		||||
			// depending on whether they are exiting/entering overview, or just
 | 
			
		||||
			// moving from slide to slide
 | 
			
		||||
			this.Reveal.getRevealElement().classList.add( 'overview-deactivating' );
 | 
			
		||||
 | 
			
		||||
			setTimeout( () => {
 | 
			
		||||
				this.Reveal.getRevealElement().classList.remove( 'overview-deactivating' );
 | 
			
		||||
			}, 1 );
 | 
			
		||||
 | 
			
		||||
			// Move the background element back out
 | 
			
		||||
			this.Reveal.getRevealElement().appendChild( this.Reveal.getBackgroundsElement() );
 | 
			
		||||
 | 
			
		||||
			// Clean up changes made to slides
 | 
			
		||||
			queryAll( this.Reveal.getRevealElement(), SLIDES_SELECTOR ).forEach( slide => {
 | 
			
		||||
				transformElement( slide, '' );
 | 
			
		||||
 | 
			
		||||
				slide.removeEventListener( 'click', this.onSlideClicked, true );
 | 
			
		||||
			} );
 | 
			
		||||
 | 
			
		||||
			// Clean up changes made to backgrounds
 | 
			
		||||
			queryAll( this.Reveal.getBackgroundsElement(), '.slide-background' ).forEach( background => {
 | 
			
		||||
				transformElement( background, '' );
 | 
			
		||||
			} );
 | 
			
		||||
 | 
			
		||||
			this.Reveal.transformSlides( { overview: '' } );
 | 
			
		||||
 | 
			
		||||
			const indices = this.Reveal.getIndices();
 | 
			
		||||
 | 
			
		||||
			this.Reveal.slide( indices.h, indices.v );
 | 
			
		||||
			this.Reveal.layout();
 | 
			
		||||
			this.Reveal.cueAutoSlide();
 | 
			
		||||
 | 
			
		||||
			// Notify observers of the overview hiding
 | 
			
		||||
			this.Reveal.dispatchEvent({
 | 
			
		||||
				type: 'overviewhidden',
 | 
			
		||||
				data: {
 | 
			
		||||
					'indexh': indices.h,
 | 
			
		||||
					'indexv': indices.v,
 | 
			
		||||
					'currentSlide': this.Reveal.getCurrentSlide()
 | 
			
		||||
				}
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Toggles the slide overview mode on and off.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param {Boolean} [override] Flag which overrides the
 | 
			
		||||
	 * toggle logic and forcibly sets the desired state. True means
 | 
			
		||||
	 * overview is open, false means it's closed.
 | 
			
		||||
	 */
 | 
			
		||||
	toggle( override ) {
 | 
			
		||||
 | 
			
		||||
		if( typeof override === 'boolean' ) {
 | 
			
		||||
			override ? this.activate() : this.deactivate();
 | 
			
		||||
		}
 | 
			
		||||
		else {
 | 
			
		||||
			this.isActive() ? this.deactivate() : this.activate();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Checks if the overview is currently active.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @return {Boolean} true if the overview is active,
 | 
			
		||||
	 * false otherwise
 | 
			
		||||
	 */
 | 
			
		||||
	isActive() {
 | 
			
		||||
 | 
			
		||||
		return this.active;
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Invoked when a slide is and we're in the overview.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param {object} event
 | 
			
		||||
	 */
 | 
			
		||||
	onSlideClicked( event ) {
 | 
			
		||||
 | 
			
		||||
		if( this.isActive() ) {
 | 
			
		||||
			event.preventDefault();
 | 
			
		||||
 | 
			
		||||
			let element = event.target;
 | 
			
		||||
 | 
			
		||||
			while( element && !element.nodeName.match( /section/gi ) ) {
 | 
			
		||||
				element = element.parentNode;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if( element && !element.classList.contains( 'disabled' ) ) {
 | 
			
		||||
 | 
			
		||||
				this.deactivate();
 | 
			
		||||
 | 
			
		||||
				if( element.nodeName.match( /section/gi ) ) {
 | 
			
		||||
					let h = parseInt( element.getAttribute( 'data-index-h' ), 10 ),
 | 
			
		||||
						v = parseInt( element.getAttribute( 'data-index-v' ), 10 );
 | 
			
		||||
 | 
			
		||||
					this.Reveal.slide( h, v );
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										254
									
								
								js/controllers/plugins.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										254
									
								
								js/controllers/plugins.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,254 @@
 | 
			
		||||
import { loadScript } from '../utils/loader.js'
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Manages loading and registering of reveal.js plugins.
 | 
			
		||||
 */
 | 
			
		||||
export default class Plugins {
 | 
			
		||||
 | 
			
		||||
	constructor( reveal ) {
 | 
			
		||||
 | 
			
		||||
		this.Reveal = reveal;
 | 
			
		||||
 | 
			
		||||
		// Flags our current state (idle -> loading -> loaded)
 | 
			
		||||
		this.state = 'idle';
 | 
			
		||||
 | 
			
		||||
		// An id:instance map of currently registed plugins
 | 
			
		||||
		this.registeredPlugins = {};
 | 
			
		||||
 | 
			
		||||
		this.asyncDependencies = [];
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Loads reveal.js dependencies, registers and
 | 
			
		||||
	 * initializes plugins.
 | 
			
		||||
	 *
 | 
			
		||||
	 * Plugins are direct references to a reveal.js plugin
 | 
			
		||||
	 * object that we register and initialize after any
 | 
			
		||||
	 * synchronous dependencies have loaded.
 | 
			
		||||
	 *
 | 
			
		||||
	 * Dependencies are defined via the 'dependencies' config
 | 
			
		||||
	 * option and will be loaded prior to starting reveal.js.
 | 
			
		||||
	 * Some dependencies may have an 'async' flag, if so they
 | 
			
		||||
	 * will load after reveal.js has been started up.
 | 
			
		||||
	 */
 | 
			
		||||
	load( plugins, dependencies ) {
 | 
			
		||||
 | 
			
		||||
		this.state = 'loading';
 | 
			
		||||
 | 
			
		||||
		plugins.forEach( this.registerPlugin.bind( this ) );
 | 
			
		||||
 | 
			
		||||
		return new Promise( resolve => {
 | 
			
		||||
 | 
			
		||||
			let scripts = [],
 | 
			
		||||
				scriptsToLoad = 0;
 | 
			
		||||
 | 
			
		||||
			dependencies.forEach( s => {
 | 
			
		||||
				// Load if there's no condition or the condition is truthy
 | 
			
		||||
				if( !s.condition || s.condition() ) {
 | 
			
		||||
					if( s.async ) {
 | 
			
		||||
						this.asyncDependencies.push( s );
 | 
			
		||||
					}
 | 
			
		||||
					else {
 | 
			
		||||
						scripts.push( s );
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			} );
 | 
			
		||||
 | 
			
		||||
			if( scripts.length ) {
 | 
			
		||||
				scriptsToLoad = scripts.length;
 | 
			
		||||
 | 
			
		||||
				const scriptLoadedCallback = (s) => {
 | 
			
		||||
					if( s && typeof s.callback === 'function' ) s.callback();
 | 
			
		||||
 | 
			
		||||
					if( --scriptsToLoad === 0 ) {
 | 
			
		||||
						this.initPlugins().then( resolve );
 | 
			
		||||
					}
 | 
			
		||||
				};
 | 
			
		||||
 | 
			
		||||
				// Load synchronous scripts
 | 
			
		||||
				scripts.forEach( s => {
 | 
			
		||||
					if( typeof s.id === 'string' ) {
 | 
			
		||||
						this.registerPlugin( s );
 | 
			
		||||
						scriptLoadedCallback( s );
 | 
			
		||||
					}
 | 
			
		||||
					else if( typeof s.src === 'string' ) {
 | 
			
		||||
						loadScript( s.src, () => scriptLoadedCallback(s) );
 | 
			
		||||
					}
 | 
			
		||||
					else {
 | 
			
		||||
						console.warn( 'Unrecognized plugin format', s );
 | 
			
		||||
						scriptLoadedCallback();
 | 
			
		||||
					}
 | 
			
		||||
				} );
 | 
			
		||||
			}
 | 
			
		||||
			else {
 | 
			
		||||
				this.initPlugins().then( resolve );
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
		} );
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Initializes our plugins and waits for them to be ready
 | 
			
		||||
	 * before proceeding.
 | 
			
		||||
	 */
 | 
			
		||||
	initPlugins() {
 | 
			
		||||
 | 
			
		||||
		return new Promise( resolve => {
 | 
			
		||||
 | 
			
		||||
			let pluginValues = Object.values( this.registeredPlugins );
 | 
			
		||||
			let pluginsToInitialize = pluginValues.length;
 | 
			
		||||
 | 
			
		||||
			// If there are no plugins, skip this step
 | 
			
		||||
			if( pluginsToInitialize === 0 ) {
 | 
			
		||||
				this.loadAsync().then( resolve );
 | 
			
		||||
			}
 | 
			
		||||
			// ... otherwise initialize plugins
 | 
			
		||||
			else {
 | 
			
		||||
 | 
			
		||||
				let initNextPlugin;
 | 
			
		||||
 | 
			
		||||
				let afterPlugInitialized = () => {
 | 
			
		||||
					if( --pluginsToInitialize === 0 ) {
 | 
			
		||||
						this.loadAsync().then( resolve );
 | 
			
		||||
					}
 | 
			
		||||
					else {
 | 
			
		||||
						initNextPlugin();
 | 
			
		||||
					}
 | 
			
		||||
				};
 | 
			
		||||
 | 
			
		||||
				let i = 0;
 | 
			
		||||
 | 
			
		||||
				// Initialize plugins serially
 | 
			
		||||
				initNextPlugin = () => {
 | 
			
		||||
 | 
			
		||||
					let plugin = pluginValues[i++];
 | 
			
		||||
 | 
			
		||||
					// If the plugin has an 'init' method, invoke it
 | 
			
		||||
					if( typeof plugin.init === 'function' ) {
 | 
			
		||||
						let promise = plugin.init( this.Reveal );
 | 
			
		||||
 | 
			
		||||
						// If the plugin returned a Promise, wait for it
 | 
			
		||||
						if( promise && typeof promise.then === 'function' ) {
 | 
			
		||||
							promise.then( afterPlugInitialized );
 | 
			
		||||
						}
 | 
			
		||||
						else {
 | 
			
		||||
							afterPlugInitialized();
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
					else {
 | 
			
		||||
						afterPlugInitialized();
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				initNextPlugin();
 | 
			
		||||
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
		} )
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Loads all async reveal.js dependencies.
 | 
			
		||||
	 */
 | 
			
		||||
	loadAsync() {
 | 
			
		||||
 | 
			
		||||
		this.state = 'loaded';
 | 
			
		||||
 | 
			
		||||
		if( this.asyncDependencies.length ) {
 | 
			
		||||
			this.asyncDependencies.forEach( s => {
 | 
			
		||||
				loadScript( s.src, s.callback );
 | 
			
		||||
			} );
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return Promise.resolve();
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Registers a new plugin with this reveal.js instance.
 | 
			
		||||
	 *
 | 
			
		||||
	 * reveal.js waits for all regisered plugins to initialize
 | 
			
		||||
	 * before considering itself ready, as long as the plugin
 | 
			
		||||
	 * is registered before calling `Reveal.initialize()`.
 | 
			
		||||
	 */
 | 
			
		||||
	registerPlugin( plugin ) {
 | 
			
		||||
 | 
			
		||||
		// Backwards compatibility to make reveal.js ~3.9.0
 | 
			
		||||
		// plugins work with reveal.js 4.0.0
 | 
			
		||||
		if( arguments.length === 2 && typeof arguments[0] === 'string' ) {
 | 
			
		||||
			plugin = arguments[1];
 | 
			
		||||
			plugin.id = arguments[0];
 | 
			
		||||
		}
 | 
			
		||||
		// Plugin can optionally be a function which we call
 | 
			
		||||
		// to create an instance of the plugin
 | 
			
		||||
		else if( typeof plugin === 'function' ) {
 | 
			
		||||
			plugin = plugin();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		let id = plugin.id;
 | 
			
		||||
 | 
			
		||||
		if( typeof id !== 'string' ) {
 | 
			
		||||
			console.warn( 'Unrecognized plugin format; can\'t find plugin.id', plugin );
 | 
			
		||||
		}
 | 
			
		||||
		else if( this.registeredPlugins[id] === undefined ) {
 | 
			
		||||
			this.registeredPlugins[id] = plugin;
 | 
			
		||||
 | 
			
		||||
			// If a plugin is registered after reveal.js is loaded,
 | 
			
		||||
			// initialize it right away
 | 
			
		||||
			if( this.state === 'loaded' && typeof plugin.init === 'function' ) {
 | 
			
		||||
				plugin.init( this.Reveal );
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		else {
 | 
			
		||||
			console.warn( 'reveal.js: "'+ id +'" plugin has already been registered' );
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Checks if a specific plugin has been registered.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param {String} id Unique plugin identifier
 | 
			
		||||
	 */
 | 
			
		||||
	hasPlugin( id ) {
 | 
			
		||||
 | 
			
		||||
		return !!this.registeredPlugins[id];
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Returns the specific plugin instance, if a plugin
 | 
			
		||||
	 * with the given ID has been registered.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param {String} id Unique plugin identifier
 | 
			
		||||
	 */
 | 
			
		||||
	getPlugin( id ) {
 | 
			
		||||
 | 
			
		||||
		return this.registeredPlugins[id];
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	getRegisteredPlugins() {
 | 
			
		||||
 | 
			
		||||
		return this.registeredPlugins;
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	destroy() {
 | 
			
		||||
 | 
			
		||||
		Object.values( this.registeredPlugins ).forEach( plugin => {
 | 
			
		||||
			if( typeof plugin.destroy === 'function' ) {
 | 
			
		||||
				plugin.destroy();
 | 
			
		||||
			}
 | 
			
		||||
		} );
 | 
			
		||||
 | 
			
		||||
		this.registeredPlugins = {};
 | 
			
		||||
		this.asyncDependencies = [];
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										129
									
								
								js/controllers/pointer.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								js/controllers/pointer.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,129 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Handles hiding of the pointer/cursor when inactive.
 | 
			
		||||
 */
 | 
			
		||||
export default class Pointer {
 | 
			
		||||
 | 
			
		||||
	constructor( Reveal ) {
 | 
			
		||||
 | 
			
		||||
		this.Reveal = Reveal;
 | 
			
		||||
 | 
			
		||||
		// Throttles mouse wheel navigation
 | 
			
		||||
		this.lastMouseWheelStep = 0;
 | 
			
		||||
 | 
			
		||||
		// Is the mouse pointer currently hidden from view
 | 
			
		||||
		this.cursorHidden = false;
 | 
			
		||||
 | 
			
		||||
		// Timeout used to determine when the cursor is inactive
 | 
			
		||||
		this.cursorInactiveTimeout = 0;
 | 
			
		||||
 | 
			
		||||
		this.onDocumentCursorActive = this.onDocumentCursorActive.bind( this );
 | 
			
		||||
		this.onDocumentMouseScroll = this.onDocumentMouseScroll.bind( this );
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Called when the reveal.js config is updated.
 | 
			
		||||
	 */
 | 
			
		||||
	configure( config, oldConfig ) {
 | 
			
		||||
 | 
			
		||||
		if( config.mouseWheel ) {
 | 
			
		||||
			document.addEventListener( 'DOMMouseScroll', this.onDocumentMouseScroll, false ); // FF
 | 
			
		||||
			document.addEventListener( 'mousewheel', this.onDocumentMouseScroll, false );
 | 
			
		||||
		}
 | 
			
		||||
		else {
 | 
			
		||||
			document.removeEventListener( 'DOMMouseScroll', this.onDocumentMouseScroll, false ); // FF
 | 
			
		||||
			document.removeEventListener( 'mousewheel', this.onDocumentMouseScroll, false );
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Auto-hide the mouse pointer when its inactive
 | 
			
		||||
		if( config.hideInactiveCursor ) {
 | 
			
		||||
			document.addEventListener( 'mousemove', this.onDocumentCursorActive, false );
 | 
			
		||||
			document.addEventListener( 'mousedown', this.onDocumentCursorActive, false );
 | 
			
		||||
		}
 | 
			
		||||
		else {
 | 
			
		||||
			this.showCursor();
 | 
			
		||||
 | 
			
		||||
			document.removeEventListener( 'mousemove', this.onDocumentCursorActive, false );
 | 
			
		||||
			document.removeEventListener( 'mousedown', this.onDocumentCursorActive, false );
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Shows the mouse pointer after it has been hidden with
 | 
			
		||||
	 * #hideCursor.
 | 
			
		||||
	 */
 | 
			
		||||
	showCursor() {
 | 
			
		||||
 | 
			
		||||
		if( this.cursorHidden ) {
 | 
			
		||||
			this.cursorHidden = false;
 | 
			
		||||
			this.Reveal.getRevealElement().style.cursor = '';
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Hides the mouse pointer when it's on top of the .reveal
 | 
			
		||||
	 * container.
 | 
			
		||||
	 */
 | 
			
		||||
	hideCursor() {
 | 
			
		||||
 | 
			
		||||
		if( this.cursorHidden === false ) {
 | 
			
		||||
			this.cursorHidden = true;
 | 
			
		||||
			this.Reveal.getRevealElement().style.cursor = 'none';
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	destroy() {
 | 
			
		||||
 | 
			
		||||
		this.showCursor();
 | 
			
		||||
 | 
			
		||||
		document.removeEventListener( 'DOMMouseScroll', this.onDocumentMouseScroll, false );
 | 
			
		||||
		document.removeEventListener( 'mousewheel', this.onDocumentMouseScroll, false );
 | 
			
		||||
		document.removeEventListener( 'mousemove', this.onDocumentCursorActive, false );
 | 
			
		||||
		document.removeEventListener( 'mousedown', this.onDocumentCursorActive, false );
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Called whenever there is mouse input at the document level
 | 
			
		||||
	 * to determine if the cursor is active or not.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param {object} event
 | 
			
		||||
	 */
 | 
			
		||||
	onDocumentCursorActive( event ) {
 | 
			
		||||
 | 
			
		||||
		this.showCursor();
 | 
			
		||||
 | 
			
		||||
		clearTimeout( this.cursorInactiveTimeout );
 | 
			
		||||
 | 
			
		||||
		this.cursorInactiveTimeout = setTimeout( this.hideCursor.bind( this ), this.Reveal.getConfig().hideCursorTime );
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Handles mouse wheel scrolling, throttled to avoid skipping
 | 
			
		||||
	 * multiple slides.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param {object} event
 | 
			
		||||
	 */
 | 
			
		||||
	onDocumentMouseScroll( event ) {
 | 
			
		||||
 | 
			
		||||
		if( Date.now() - this.lastMouseWheelStep > 1000 ) {
 | 
			
		||||
 | 
			
		||||
			this.lastMouseWheelStep = Date.now();
 | 
			
		||||
 | 
			
		||||
			let delta = event.detail || -event.wheelDelta;
 | 
			
		||||
			if( delta > 0 ) {
 | 
			
		||||
				this.Reveal.next();
 | 
			
		||||
			}
 | 
			
		||||
			else if( delta < 0 ) {
 | 
			
		||||
				this.Reveal.prev();
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										236
									
								
								js/controllers/print.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										236
									
								
								js/controllers/print.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,236 @@
 | 
			
		||||
import { SLIDES_SELECTOR } from '../utils/constants.js'
 | 
			
		||||
import { queryAll, createStyleSheet } from '../utils/util.js'
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Setups up our presentation for printing/exporting to PDF.
 | 
			
		||||
 */
 | 
			
		||||
export default class Print {
 | 
			
		||||
 | 
			
		||||
	constructor( Reveal ) {
 | 
			
		||||
 | 
			
		||||
		this.Reveal = Reveal;
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Configures the presentation for printing to a static
 | 
			
		||||
	 * PDF.
 | 
			
		||||
	 */
 | 
			
		||||
	async setupPDF() {
 | 
			
		||||
 | 
			
		||||
		const config = this.Reveal.getConfig();
 | 
			
		||||
		const slides = queryAll( this.Reveal.getRevealElement(), SLIDES_SELECTOR )
 | 
			
		||||
 | 
			
		||||
		// Compute slide numbers now, before we start duplicating slides
 | 
			
		||||
		const injectPageNumbers = config.slideNumber && /all|print/i.test( config.showSlideNumber );
 | 
			
		||||
 | 
			
		||||
		const slideSize = this.Reveal.getComputedSlideSize( window.innerWidth, window.innerHeight );
 | 
			
		||||
 | 
			
		||||
		// Dimensions of the PDF pages
 | 
			
		||||
		const pageWidth = Math.floor( slideSize.width * ( 1 + config.margin ) ),
 | 
			
		||||
			pageHeight = Math.floor( slideSize.height * ( 1 + config.margin ) );
 | 
			
		||||
 | 
			
		||||
		// Dimensions of slides within the pages
 | 
			
		||||
		const slideWidth = slideSize.width,
 | 
			
		||||
			slideHeight = slideSize.height;
 | 
			
		||||
 | 
			
		||||
		await new Promise( requestAnimationFrame );
 | 
			
		||||
 | 
			
		||||
		// Let the browser know what page size we want to print
 | 
			
		||||
		createStyleSheet( '@page{size:'+ pageWidth +'px '+ pageHeight +'px; margin: 0px;}' );
 | 
			
		||||
 | 
			
		||||
		// Limit the size of certain elements to the dimensions of the slide
 | 
			
		||||
		createStyleSheet( '.reveal section>img, .reveal section>video, .reveal section>iframe{max-width: '+ slideWidth +'px; max-height:'+ slideHeight +'px}' );
 | 
			
		||||
 | 
			
		||||
		document.documentElement.classList.add( 'print-pdf' );
 | 
			
		||||
		document.body.style.width = pageWidth + 'px';
 | 
			
		||||
		document.body.style.height = pageHeight + 'px';
 | 
			
		||||
 | 
			
		||||
		const viewportElement = document.querySelector( '.reveal-viewport' );
 | 
			
		||||
		let presentationBackground;
 | 
			
		||||
		if( viewportElement ) {
 | 
			
		||||
			const viewportStyles = window.getComputedStyle( viewportElement );
 | 
			
		||||
			if( viewportStyles && viewportStyles.background ) {
 | 
			
		||||
				presentationBackground = viewportStyles.background;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Make sure stretch elements fit on slide
 | 
			
		||||
		await new Promise( requestAnimationFrame );
 | 
			
		||||
		this.Reveal.layoutSlideContents( slideWidth, slideHeight );
 | 
			
		||||
 | 
			
		||||
		// Batch scrollHeight access to prevent layout thrashing
 | 
			
		||||
		await new Promise( requestAnimationFrame );
 | 
			
		||||
 | 
			
		||||
		const slideScrollHeights = slides.map( slide => slide.scrollHeight );
 | 
			
		||||
 | 
			
		||||
		const pages = [];
 | 
			
		||||
		const pageContainer = slides[0].parentNode;
 | 
			
		||||
		let slideNumber = 1;
 | 
			
		||||
 | 
			
		||||
		// Slide and slide background layout
 | 
			
		||||
		slides.forEach( function( slide, index ) {
 | 
			
		||||
 | 
			
		||||
			// Vertical stacks are not centred since their section
 | 
			
		||||
			// children will be
 | 
			
		||||
			if( slide.classList.contains( 'stack' ) === false ) {
 | 
			
		||||
				// Center the slide inside of the page, giving the slide some margin
 | 
			
		||||
				let left = ( pageWidth - slideWidth ) / 2;
 | 
			
		||||
				let top = ( pageHeight - slideHeight ) / 2;
 | 
			
		||||
 | 
			
		||||
				const contentHeight = slideScrollHeights[ index ];
 | 
			
		||||
				let numberOfPages = Math.max( Math.ceil( contentHeight / pageHeight ), 1 );
 | 
			
		||||
 | 
			
		||||
				// Adhere to configured pages per slide limit
 | 
			
		||||
				numberOfPages = Math.min( numberOfPages, config.pdfMaxPagesPerSlide );
 | 
			
		||||
 | 
			
		||||
				// Center slides vertically
 | 
			
		||||
				if( numberOfPages === 1 && config.center || slide.classList.contains( 'center' ) ) {
 | 
			
		||||
					top = Math.max( ( pageHeight - contentHeight ) / 2, 0 );
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				// Wrap the slide in a page element and hide its overflow
 | 
			
		||||
				// so that no page ever flows onto another
 | 
			
		||||
				const page = document.createElement( 'div' );
 | 
			
		||||
				pages.push( page );
 | 
			
		||||
 | 
			
		||||
				page.className = 'pdf-page';
 | 
			
		||||
				page.style.height = ( ( pageHeight + config.pdfPageHeightOffset ) * numberOfPages ) + 'px';
 | 
			
		||||
 | 
			
		||||
				// Copy the presentation-wide background to each individual
 | 
			
		||||
				// page when printing
 | 
			
		||||
				if( presentationBackground ) {
 | 
			
		||||
					page.style.background = presentationBackground;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				page.appendChild( slide );
 | 
			
		||||
 | 
			
		||||
				// Position the slide inside of the page
 | 
			
		||||
				slide.style.left = left + 'px';
 | 
			
		||||
				slide.style.top = top + 'px';
 | 
			
		||||
				slide.style.width = slideWidth + 'px';
 | 
			
		||||
 | 
			
		||||
				// Re-run the slide layout so that r-fit-text is applied based on
 | 
			
		||||
				// the printed slide size
 | 
			
		||||
				this.Reveal.slideContent.layout( slide )
 | 
			
		||||
 | 
			
		||||
				if( slide.slideBackgroundElement ) {
 | 
			
		||||
					page.insertBefore( slide.slideBackgroundElement, slide );
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				// Inject notes if `showNotes` is enabled
 | 
			
		||||
				if( config.showNotes ) {
 | 
			
		||||
 | 
			
		||||
					// Are there notes for this slide?
 | 
			
		||||
					const notes = this.Reveal.getSlideNotes( slide );
 | 
			
		||||
					if( notes ) {
 | 
			
		||||
 | 
			
		||||
						const notesSpacing = 8;
 | 
			
		||||
						const notesLayout = typeof config.showNotes === 'string' ? config.showNotes : 'inline';
 | 
			
		||||
						const notesElement = document.createElement( 'div' );
 | 
			
		||||
						notesElement.classList.add( 'speaker-notes' );
 | 
			
		||||
						notesElement.classList.add( 'speaker-notes-pdf' );
 | 
			
		||||
						notesElement.setAttribute( 'data-layout', notesLayout );
 | 
			
		||||
						notesElement.innerHTML = notes;
 | 
			
		||||
 | 
			
		||||
						if( notesLayout === 'separate-page' ) {
 | 
			
		||||
							pages.push( notesElement );
 | 
			
		||||
						}
 | 
			
		||||
						else {
 | 
			
		||||
							notesElement.style.left = notesSpacing + 'px';
 | 
			
		||||
							notesElement.style.bottom = notesSpacing + 'px';
 | 
			
		||||
							notesElement.style.width = ( pageWidth - notesSpacing*2 ) + 'px';
 | 
			
		||||
							page.appendChild( notesElement );
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				// Inject page numbers if `slideNumbers` are enabled
 | 
			
		||||
				if( injectPageNumbers ) {
 | 
			
		||||
					const numberElement = document.createElement( 'div' );
 | 
			
		||||
					numberElement.classList.add( 'slide-number' );
 | 
			
		||||
					numberElement.classList.add( 'slide-number-pdf' );
 | 
			
		||||
					numberElement.innerHTML = slideNumber++;
 | 
			
		||||
					page.appendChild( numberElement );
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				// Copy page and show fragments one after another
 | 
			
		||||
				if( config.pdfSeparateFragments ) {
 | 
			
		||||
 | 
			
		||||
					// Each fragment 'group' is an array containing one or more
 | 
			
		||||
					// fragments. Multiple fragments that appear at the same time
 | 
			
		||||
					// are part of the same group.
 | 
			
		||||
					const fragmentGroups = this.Reveal.fragments.sort( page.querySelectorAll( '.fragment' ), true );
 | 
			
		||||
 | 
			
		||||
					let previousFragmentStep;
 | 
			
		||||
 | 
			
		||||
					fragmentGroups.forEach( function( fragments, index ) {
 | 
			
		||||
 | 
			
		||||
						// Remove 'current-fragment' from the previous group
 | 
			
		||||
						if( previousFragmentStep ) {
 | 
			
		||||
							previousFragmentStep.forEach( function( fragment ) {
 | 
			
		||||
								fragment.classList.remove( 'current-fragment' );
 | 
			
		||||
							} );
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						// Show the fragments for the current index
 | 
			
		||||
						fragments.forEach( function( fragment ) {
 | 
			
		||||
							fragment.classList.add( 'visible', 'current-fragment' );
 | 
			
		||||
						}, this );
 | 
			
		||||
 | 
			
		||||
						// Create a separate page for the current fragment state
 | 
			
		||||
						const clonedPage = page.cloneNode( true );
 | 
			
		||||
 | 
			
		||||
						// Inject unique page numbers for fragments
 | 
			
		||||
						if( injectPageNumbers ) {
 | 
			
		||||
							const numberElement = clonedPage.querySelector( '.slide-number-pdf' );
 | 
			
		||||
							const fragmentNumber = index + 1;
 | 
			
		||||
							numberElement.innerHTML += '.' + fragmentNumber;
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						pages.push( clonedPage );
 | 
			
		||||
 | 
			
		||||
						previousFragmentStep = fragments;
 | 
			
		||||
 | 
			
		||||
					}, this );
 | 
			
		||||
 | 
			
		||||
					// Reset the first/original page so that all fragments are hidden
 | 
			
		||||
					fragmentGroups.forEach( function( fragments ) {
 | 
			
		||||
						fragments.forEach( function( fragment ) {
 | 
			
		||||
							fragment.classList.remove( 'visible', 'current-fragment' );
 | 
			
		||||
						} );
 | 
			
		||||
					} );
 | 
			
		||||
 | 
			
		||||
				}
 | 
			
		||||
				// Show all fragments
 | 
			
		||||
				else {
 | 
			
		||||
					queryAll( page, '.fragment:not(.fade-out)' ).forEach( function( fragment ) {
 | 
			
		||||
						fragment.classList.add( 'visible' );
 | 
			
		||||
					} );
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
		}, this );
 | 
			
		||||
 | 
			
		||||
		await new Promise( requestAnimationFrame );
 | 
			
		||||
 | 
			
		||||
		pages.forEach( page => pageContainer.appendChild( page ) );
 | 
			
		||||
 | 
			
		||||
		// Notify subscribers that the PDF layout is good to go
 | 
			
		||||
		this.Reveal.dispatchEvent({ type: 'pdf-ready' });
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Checks if this instance is being used to print a PDF.
 | 
			
		||||
	 */
 | 
			
		||||
	isPrintingPDF() {
 | 
			
		||||
 | 
			
		||||
		return ( /print-pdf/gi ).test( window.location.search );
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										110
									
								
								js/controllers/progress.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								js/controllers/progress.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,110 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Creates a visual progress bar for the presentation.
 | 
			
		||||
 */
 | 
			
		||||
export default class Progress {
 | 
			
		||||
 | 
			
		||||
	constructor( Reveal ) {
 | 
			
		||||
 | 
			
		||||
		this.Reveal = Reveal;
 | 
			
		||||
 | 
			
		||||
		this.onProgressClicked = this.onProgressClicked.bind( this );
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render() {
 | 
			
		||||
 | 
			
		||||
		this.element = document.createElement( 'div' );
 | 
			
		||||
		this.element.className = 'progress';
 | 
			
		||||
		this.Reveal.getRevealElement().appendChild( this.element );
 | 
			
		||||
 | 
			
		||||
		this.bar = document.createElement( 'span' );
 | 
			
		||||
		this.element.appendChild( this.bar );
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Called when the reveal.js config is updated.
 | 
			
		||||
	 */
 | 
			
		||||
	configure( config, oldConfig ) {
 | 
			
		||||
 | 
			
		||||
		this.element.style.display = config.progress ? 'block' : 'none';
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	bind() {
 | 
			
		||||
 | 
			
		||||
		if( this.Reveal.getConfig().progress && this.element ) {
 | 
			
		||||
			this.element.addEventListener( 'click', this.onProgressClicked, false );
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	unbind() {
 | 
			
		||||
 | 
			
		||||
		if ( this.Reveal.getConfig().progress && this.element ) {
 | 
			
		||||
			this.element.removeEventListener( 'click', this.onProgressClicked, false );
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Updates the progress bar to reflect the current slide.
 | 
			
		||||
	 */
 | 
			
		||||
	update() {
 | 
			
		||||
 | 
			
		||||
		// Update progress if enabled
 | 
			
		||||
		if( this.Reveal.getConfig().progress && this.bar ) {
 | 
			
		||||
 | 
			
		||||
			let scale = this.Reveal.getProgress();
 | 
			
		||||
 | 
			
		||||
			// Don't fill the progress bar if there's only one slide
 | 
			
		||||
			if( this.Reveal.getTotalSlides() < 2 ) {
 | 
			
		||||
				scale = 0;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			this.bar.style.transform = 'scaleX('+ scale +')';
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	getMaxWidth() {
 | 
			
		||||
 | 
			
		||||
		return this.Reveal.getRevealElement().offsetWidth;
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Clicking on the progress bar results in a navigation to the
 | 
			
		||||
	 * closest approximate horizontal slide using this equation:
 | 
			
		||||
	 *
 | 
			
		||||
	 * ( clickX / presentationWidth ) * numberOfSlides
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param {object} event
 | 
			
		||||
	 */
 | 
			
		||||
	onProgressClicked( event ) {
 | 
			
		||||
 | 
			
		||||
		this.Reveal.onUserInput( event );
 | 
			
		||||
 | 
			
		||||
		event.preventDefault();
 | 
			
		||||
 | 
			
		||||
		let slides = this.Reveal.getSlides();
 | 
			
		||||
		let slidesTotal = slides.length;
 | 
			
		||||
		let slideIndex = Math.floor( ( event.clientX / this.getMaxWidth() ) * slidesTotal );
 | 
			
		||||
 | 
			
		||||
		if( this.Reveal.getConfig().rtl ) {
 | 
			
		||||
			slideIndex = slidesTotal - slideIndex;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		let targetIndices = this.Reveal.getIndices(slides[slideIndex]);
 | 
			
		||||
		this.Reveal.slide( targetIndices.h, targetIndices.v );
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	destroy() {
 | 
			
		||||
 | 
			
		||||
		this.element.remove();
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										479
									
								
								js/controllers/slidecontent.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										479
									
								
								js/controllers/slidecontent.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,479 @@
 | 
			
		||||
import { extend, queryAll, closest, getMimeTypeFromFile } from '../utils/util.js'
 | 
			
		||||
import { isMobile } from '../utils/device.js'
 | 
			
		||||
 | 
			
		||||
import fitty from 'fitty';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Handles loading, unloading and playback of slide
 | 
			
		||||
 * content such as images, videos and iframes.
 | 
			
		||||
 */
 | 
			
		||||
export default class SlideContent {
 | 
			
		||||
 | 
			
		||||
	constructor( Reveal ) {
 | 
			
		||||
 | 
			
		||||
		this.Reveal = Reveal;
 | 
			
		||||
 | 
			
		||||
		this.startEmbeddedIframe = this.startEmbeddedIframe.bind( this );
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Should the given element be preloaded?
 | 
			
		||||
	 * Decides based on local element attributes and global config.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param {HTMLElement} element
 | 
			
		||||
	 */
 | 
			
		||||
	shouldPreload( element ) {
 | 
			
		||||
 | 
			
		||||
		// Prefer an explicit global preload setting
 | 
			
		||||
		let preload = this.Reveal.getConfig().preloadIframes;
 | 
			
		||||
 | 
			
		||||
		// If no global setting is available, fall back on the element's
 | 
			
		||||
		// own preload setting
 | 
			
		||||
		if( typeof preload !== 'boolean' ) {
 | 
			
		||||
			preload = element.hasAttribute( 'data-preload' );
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return preload;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Called when the given slide is within the configured view
 | 
			
		||||
	 * distance. Shows the slide element and loads any content
 | 
			
		||||
	 * that is set to load lazily (data-src).
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param {HTMLElement} slide Slide to show
 | 
			
		||||
	 */
 | 
			
		||||
	load( slide, options = {} ) {
 | 
			
		||||
 | 
			
		||||
		// Show the slide element
 | 
			
		||||
		slide.style.display = this.Reveal.getConfig().display;
 | 
			
		||||
 | 
			
		||||
		// Media elements with data-src attributes
 | 
			
		||||
		queryAll( slide, 'img[data-src], video[data-src], audio[data-src], iframe[data-src]' ).forEach( element => {
 | 
			
		||||
			if( element.tagName !== 'IFRAME' || this.shouldPreload( element ) ) {
 | 
			
		||||
				element.setAttribute( 'src', element.getAttribute( 'data-src' ) );
 | 
			
		||||
				element.setAttribute( 'data-lazy-loaded', '' );
 | 
			
		||||
				element.removeAttribute( 'data-src' );
 | 
			
		||||
			}
 | 
			
		||||
		} );
 | 
			
		||||
 | 
			
		||||
		// Media elements with <source> children
 | 
			
		||||
		queryAll( slide, 'video, audio' ).forEach( media => {
 | 
			
		||||
			let sources = 0;
 | 
			
		||||
 | 
			
		||||
			queryAll( media, 'source[data-src]' ).forEach( source => {
 | 
			
		||||
				source.setAttribute( 'src', source.getAttribute( 'data-src' ) );
 | 
			
		||||
				source.removeAttribute( 'data-src' );
 | 
			
		||||
				source.setAttribute( 'data-lazy-loaded', '' );
 | 
			
		||||
				sources += 1;
 | 
			
		||||
			} );
 | 
			
		||||
 | 
			
		||||
			// Enable inline video playback in mobile Safari
 | 
			
		||||
			if( isMobile && media.tagName === 'VIDEO' ) {
 | 
			
		||||
				media.setAttribute( 'playsinline', '' );
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// If we rewrote sources for this video/audio element, we need
 | 
			
		||||
			// to manually tell it to load from its new origin
 | 
			
		||||
			if( sources > 0 ) {
 | 
			
		||||
				media.load();
 | 
			
		||||
			}
 | 
			
		||||
		} );
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		// Show the corresponding background element
 | 
			
		||||
		let background = slide.slideBackgroundElement;
 | 
			
		||||
		if( background ) {
 | 
			
		||||
			background.style.display = 'block';
 | 
			
		||||
 | 
			
		||||
			let backgroundContent = slide.slideBackgroundContentElement;
 | 
			
		||||
			let backgroundIframe = slide.getAttribute( 'data-background-iframe' );
 | 
			
		||||
 | 
			
		||||
			// If the background contains media, load it
 | 
			
		||||
			if( background.hasAttribute( 'data-loaded' ) === false ) {
 | 
			
		||||
				background.setAttribute( 'data-loaded', 'true' );
 | 
			
		||||
 | 
			
		||||
				let backgroundImage = slide.getAttribute( 'data-background-image' ),
 | 
			
		||||
					backgroundVideo = slide.getAttribute( 'data-background-video' ),
 | 
			
		||||
					backgroundVideoLoop = slide.hasAttribute( 'data-background-video-loop' ),
 | 
			
		||||
					backgroundVideoMuted = slide.hasAttribute( 'data-background-video-muted' );
 | 
			
		||||
 | 
			
		||||
				// Images
 | 
			
		||||
				if( backgroundImage ) {
 | 
			
		||||
					// base64
 | 
			
		||||
					if(  /^data:/.test( backgroundImage.trim() ) ) {
 | 
			
		||||
						backgroundContent.style.backgroundImage = `url(${backgroundImage.trim()})`;
 | 
			
		||||
					}
 | 
			
		||||
					// URL(s)
 | 
			
		||||
					else {
 | 
			
		||||
						backgroundContent.style.backgroundImage = backgroundImage.split( ',' ).map( background => {
 | 
			
		||||
							return `url(${encodeURI(background.trim())})`;
 | 
			
		||||
						}).join( ',' );
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
				// Videos
 | 
			
		||||
				else if ( backgroundVideo && !this.Reveal.isSpeakerNotes() ) {
 | 
			
		||||
					let video = document.createElement( 'video' );
 | 
			
		||||
 | 
			
		||||
					if( backgroundVideoLoop ) {
 | 
			
		||||
						video.setAttribute( 'loop', '' );
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					if( backgroundVideoMuted ) {
 | 
			
		||||
						video.muted = true;
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					// Enable inline playback in mobile Safari
 | 
			
		||||
					//
 | 
			
		||||
					// Mute is required for video to play when using
 | 
			
		||||
					// swipe gestures to navigate since they don't
 | 
			
		||||
					// count as direct user actions :'(
 | 
			
		||||
					if( isMobile ) {
 | 
			
		||||
						video.muted = true;
 | 
			
		||||
						video.setAttribute( 'playsinline', '' );
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					// Support comma separated lists of video sources
 | 
			
		||||
					backgroundVideo.split( ',' ).forEach( source => {
 | 
			
		||||
						let type = getMimeTypeFromFile( source );
 | 
			
		||||
						if( type ) {
 | 
			
		||||
							video.innerHTML += `<source src="${source}" type="${type}">`;
 | 
			
		||||
						}
 | 
			
		||||
						else {
 | 
			
		||||
							video.innerHTML += `<source src="${source}">`;
 | 
			
		||||
						}
 | 
			
		||||
					} );
 | 
			
		||||
 | 
			
		||||
					backgroundContent.appendChild( video );
 | 
			
		||||
				}
 | 
			
		||||
				// Iframes
 | 
			
		||||
				else if( backgroundIframe && options.excludeIframes !== true ) {
 | 
			
		||||
					let iframe = document.createElement( 'iframe' );
 | 
			
		||||
					iframe.setAttribute( 'allowfullscreen', '' );
 | 
			
		||||
					iframe.setAttribute( 'mozallowfullscreen', '' );
 | 
			
		||||
					iframe.setAttribute( 'webkitallowfullscreen', '' );
 | 
			
		||||
					iframe.setAttribute( 'allow', 'autoplay' );
 | 
			
		||||
 | 
			
		||||
					iframe.setAttribute( 'data-src', backgroundIframe );
 | 
			
		||||
 | 
			
		||||
					iframe.style.width  = '100%';
 | 
			
		||||
					iframe.style.height = '100%';
 | 
			
		||||
					iframe.style.maxHeight = '100%';
 | 
			
		||||
					iframe.style.maxWidth = '100%';
 | 
			
		||||
 | 
			
		||||
					backgroundContent.appendChild( iframe );
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// Start loading preloadable iframes
 | 
			
		||||
			let backgroundIframeElement = backgroundContent.querySelector( 'iframe[data-src]' );
 | 
			
		||||
			if( backgroundIframeElement ) {
 | 
			
		||||
 | 
			
		||||
				// Check if this iframe is eligible to be preloaded
 | 
			
		||||
				if( this.shouldPreload( background ) && !/autoplay=(1|true|yes)/gi.test( backgroundIframe ) ) {
 | 
			
		||||
					if( backgroundIframeElement.getAttribute( 'src' ) !== backgroundIframe ) {
 | 
			
		||||
						backgroundIframeElement.setAttribute( 'src', backgroundIframe );
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		this.layout( slide );
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Applies JS-dependent layout helpers for the given slide,
 | 
			
		||||
	 * if there are any.
 | 
			
		||||
	 */
 | 
			
		||||
	layout( slide ) {
 | 
			
		||||
 | 
			
		||||
		// Autosize text with the r-fit-text class based on the
 | 
			
		||||
		// size of its container. This needs to happen after the
 | 
			
		||||
		// slide is visible in order to measure the text.
 | 
			
		||||
		Array.from( slide.querySelectorAll( '.r-fit-text' ) ).forEach( element => {
 | 
			
		||||
			fitty( element, {
 | 
			
		||||
				minSize: 24,
 | 
			
		||||
				maxSize: this.Reveal.getConfig().height * 0.8,
 | 
			
		||||
				observeMutations: false,
 | 
			
		||||
				observeWindow: false
 | 
			
		||||
			} );
 | 
			
		||||
		} );
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Unloads and hides the given slide. This is called when the
 | 
			
		||||
	 * slide is moved outside of the configured view distance.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param {HTMLElement} slide
 | 
			
		||||
	 */
 | 
			
		||||
	unload( slide ) {
 | 
			
		||||
 | 
			
		||||
		// Hide the slide element
 | 
			
		||||
		slide.style.display = 'none';
 | 
			
		||||
 | 
			
		||||
		// Hide the corresponding background element
 | 
			
		||||
		let background = this.Reveal.getSlideBackground( slide );
 | 
			
		||||
		if( background ) {
 | 
			
		||||
			background.style.display = 'none';
 | 
			
		||||
 | 
			
		||||
			// Unload any background iframes
 | 
			
		||||
			queryAll( background, 'iframe[src]' ).forEach( element => {
 | 
			
		||||
				element.removeAttribute( 'src' );
 | 
			
		||||
			} );
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Reset lazy-loaded media elements with src attributes
 | 
			
		||||
		queryAll( slide, 'video[data-lazy-loaded][src], audio[data-lazy-loaded][src], iframe[data-lazy-loaded][src]' ).forEach( element => {
 | 
			
		||||
			element.setAttribute( 'data-src', element.getAttribute( 'src' ) );
 | 
			
		||||
			element.removeAttribute( 'src' );
 | 
			
		||||
		} );
 | 
			
		||||
 | 
			
		||||
		// Reset lazy-loaded media elements with <source> children
 | 
			
		||||
		queryAll( slide, 'video[data-lazy-loaded] source[src], audio source[src]' ).forEach( source => {
 | 
			
		||||
			source.setAttribute( 'data-src', source.getAttribute( 'src' ) );
 | 
			
		||||
			source.removeAttribute( 'src' );
 | 
			
		||||
		} );
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Enforces origin-specific format rules for embedded media.
 | 
			
		||||
	 */
 | 
			
		||||
	formatEmbeddedContent() {
 | 
			
		||||
 | 
			
		||||
		let _appendParamToIframeSource = ( sourceAttribute, sourceURL, param ) => {
 | 
			
		||||
			queryAll( this.Reveal.getSlidesElement(), 'iframe['+ sourceAttribute +'*="'+ sourceURL +'"]' ).forEach( el => {
 | 
			
		||||
				let src = el.getAttribute( sourceAttribute );
 | 
			
		||||
				if( src && src.indexOf( param ) === -1 ) {
 | 
			
		||||
					el.setAttribute( sourceAttribute, src + ( !/\?/.test( src ) ? '?' : '&' ) + param );
 | 
			
		||||
				}
 | 
			
		||||
			});
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		// YouTube frames must include "?enablejsapi=1"
 | 
			
		||||
		_appendParamToIframeSource( 'src', 'youtube.com/embed/', 'enablejsapi=1' );
 | 
			
		||||
		_appendParamToIframeSource( 'data-src', 'youtube.com/embed/', 'enablejsapi=1' );
 | 
			
		||||
 | 
			
		||||
		// Vimeo frames must include "?api=1"
 | 
			
		||||
		_appendParamToIframeSource( 'src', 'player.vimeo.com/', 'api=1' );
 | 
			
		||||
		_appendParamToIframeSource( 'data-src', 'player.vimeo.com/', 'api=1' );
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Start playback of any embedded content inside of
 | 
			
		||||
	 * the given element.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param {HTMLElement} element
 | 
			
		||||
	 */
 | 
			
		||||
	startEmbeddedContent( element ) {
 | 
			
		||||
 | 
			
		||||
		if( element && !this.Reveal.isSpeakerNotes() ) {
 | 
			
		||||
 | 
			
		||||
			// Restart GIFs
 | 
			
		||||
			queryAll( element, 'img[src$=".gif"]' ).forEach( el => {
 | 
			
		||||
				// Setting the same unchanged source like this was confirmed
 | 
			
		||||
				// to work in Chrome, FF & Safari
 | 
			
		||||
				el.setAttribute( 'src', el.getAttribute( 'src' ) );
 | 
			
		||||
			} );
 | 
			
		||||
 | 
			
		||||
			// HTML5 media elements
 | 
			
		||||
			queryAll( element, 'video, audio' ).forEach( el => {
 | 
			
		||||
				if( closest( el, '.fragment' ) && !closest( el, '.fragment.visible' ) ) {
 | 
			
		||||
					return;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				// Prefer an explicit global autoplay setting
 | 
			
		||||
				let autoplay = this.Reveal.getConfig().autoPlayMedia;
 | 
			
		||||
 | 
			
		||||
				// If no global setting is available, fall back on the element's
 | 
			
		||||
				// own autoplay setting
 | 
			
		||||
				if( typeof autoplay !== 'boolean' ) {
 | 
			
		||||
					autoplay = el.hasAttribute( 'data-autoplay' ) || !!closest( el, '.slide-background' );
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				if( autoplay && typeof el.play === 'function' ) {
 | 
			
		||||
 | 
			
		||||
					// If the media is ready, start playback
 | 
			
		||||
					if( el.readyState > 1 ) {
 | 
			
		||||
						this.startEmbeddedMedia( { target: el } );
 | 
			
		||||
					}
 | 
			
		||||
					// Mobile devices never fire a loaded event so instead
 | 
			
		||||
					// of waiting, we initiate playback
 | 
			
		||||
					else if( isMobile ) {
 | 
			
		||||
						let promise = el.play();
 | 
			
		||||
 | 
			
		||||
						// If autoplay does not work, ensure that the controls are visible so
 | 
			
		||||
						// that the viewer can start the media on their own
 | 
			
		||||
						if( promise && typeof promise.catch === 'function' && el.controls === false ) {
 | 
			
		||||
							promise.catch( () => {
 | 
			
		||||
								el.controls = true;
 | 
			
		||||
 | 
			
		||||
								// Once the video does start playing, hide the controls again
 | 
			
		||||
								el.addEventListener( 'play', () => {
 | 
			
		||||
									el.controls = false;
 | 
			
		||||
								} );
 | 
			
		||||
							} );
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
					// If the media isn't loaded, wait before playing
 | 
			
		||||
					else {
 | 
			
		||||
						el.removeEventListener( 'loadeddata', this.startEmbeddedMedia ); // remove first to avoid dupes
 | 
			
		||||
						el.addEventListener( 'loadeddata', this.startEmbeddedMedia );
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
				}
 | 
			
		||||
			} );
 | 
			
		||||
 | 
			
		||||
			// Normal iframes
 | 
			
		||||
			queryAll( element, 'iframe[src]' ).forEach( el => {
 | 
			
		||||
				if( closest( el, '.fragment' ) && !closest( el, '.fragment.visible' ) ) {
 | 
			
		||||
					return;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				this.startEmbeddedIframe( { target: el } );
 | 
			
		||||
			} );
 | 
			
		||||
 | 
			
		||||
			// Lazy loading iframes
 | 
			
		||||
			queryAll( element, 'iframe[data-src]' ).forEach( el => {
 | 
			
		||||
				if( closest( el, '.fragment' ) && !closest( el, '.fragment.visible' ) ) {
 | 
			
		||||
					return;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				if( el.getAttribute( 'src' ) !== el.getAttribute( 'data-src' ) ) {
 | 
			
		||||
					el.removeEventListener( 'load', this.startEmbeddedIframe ); // remove first to avoid dupes
 | 
			
		||||
					el.addEventListener( 'load', this.startEmbeddedIframe );
 | 
			
		||||
					el.setAttribute( 'src', el.getAttribute( 'data-src' ) );
 | 
			
		||||
				}
 | 
			
		||||
			} );
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Starts playing an embedded video/audio element after
 | 
			
		||||
	 * it has finished loading.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param {object} event
 | 
			
		||||
	 */
 | 
			
		||||
	startEmbeddedMedia( event ) {
 | 
			
		||||
 | 
			
		||||
		let isAttachedToDOM = !!closest( event.target, 'html' ),
 | 
			
		||||
			isVisible  		= !!closest( event.target, '.present' );
 | 
			
		||||
 | 
			
		||||
		if( isAttachedToDOM && isVisible ) {
 | 
			
		||||
			event.target.currentTime = 0;
 | 
			
		||||
			event.target.play();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		event.target.removeEventListener( 'loadeddata', this.startEmbeddedMedia );
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * "Starts" the content of an embedded iframe using the
 | 
			
		||||
	 * postMessage API.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param {object} event
 | 
			
		||||
	 */
 | 
			
		||||
	startEmbeddedIframe( event ) {
 | 
			
		||||
 | 
			
		||||
		let iframe = event.target;
 | 
			
		||||
 | 
			
		||||
		if( iframe && iframe.contentWindow ) {
 | 
			
		||||
 | 
			
		||||
			let isAttachedToDOM = !!closest( event.target, 'html' ),
 | 
			
		||||
				isVisible  		= !!closest( event.target, '.present' );
 | 
			
		||||
 | 
			
		||||
			if( isAttachedToDOM && isVisible ) {
 | 
			
		||||
 | 
			
		||||
				// Prefer an explicit global autoplay setting
 | 
			
		||||
				let autoplay = this.Reveal.getConfig().autoPlayMedia;
 | 
			
		||||
 | 
			
		||||
				// If no global setting is available, fall back on the element's
 | 
			
		||||
				// own autoplay setting
 | 
			
		||||
				if( typeof autoplay !== 'boolean' ) {
 | 
			
		||||
					autoplay = iframe.hasAttribute( 'data-autoplay' ) || !!closest( iframe, '.slide-background' );
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				// YouTube postMessage API
 | 
			
		||||
				if( /youtube\.com\/embed\//.test( iframe.getAttribute( 'src' ) ) && autoplay ) {
 | 
			
		||||
					iframe.contentWindow.postMessage( '{"event":"command","func":"playVideo","args":""}', '*' );
 | 
			
		||||
				}
 | 
			
		||||
				// Vimeo postMessage API
 | 
			
		||||
				else if( /player\.vimeo\.com\//.test( iframe.getAttribute( 'src' ) ) && autoplay ) {
 | 
			
		||||
					iframe.contentWindow.postMessage( '{"method":"play"}', '*' );
 | 
			
		||||
				}
 | 
			
		||||
				// Generic postMessage API
 | 
			
		||||
				else {
 | 
			
		||||
					iframe.contentWindow.postMessage( 'slide:start', '*' );
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Stop playback of any embedded content inside of
 | 
			
		||||
	 * the targeted slide.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param {HTMLElement} element
 | 
			
		||||
	 */
 | 
			
		||||
	stopEmbeddedContent( element, options = {} ) {
 | 
			
		||||
 | 
			
		||||
		options = extend( {
 | 
			
		||||
			// Defaults
 | 
			
		||||
			unloadIframes: true
 | 
			
		||||
		}, options );
 | 
			
		||||
 | 
			
		||||
		if( element && element.parentNode ) {
 | 
			
		||||
			// HTML5 media elements
 | 
			
		||||
			queryAll( element, 'video, audio' ).forEach( el => {
 | 
			
		||||
				if( !el.hasAttribute( 'data-ignore' ) && typeof el.pause === 'function' ) {
 | 
			
		||||
					el.setAttribute('data-paused-by-reveal', '');
 | 
			
		||||
					el.pause();
 | 
			
		||||
				}
 | 
			
		||||
			} );
 | 
			
		||||
 | 
			
		||||
			// Generic postMessage API for non-lazy loaded iframes
 | 
			
		||||
			queryAll( element, 'iframe' ).forEach( el => {
 | 
			
		||||
				if( el.contentWindow ) el.contentWindow.postMessage( 'slide:stop', '*' );
 | 
			
		||||
				el.removeEventListener( 'load', this.startEmbeddedIframe );
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			// YouTube postMessage API
 | 
			
		||||
			queryAll( element, 'iframe[src*="youtube.com/embed/"]' ).forEach( el => {
 | 
			
		||||
				if( !el.hasAttribute( 'data-ignore' ) && el.contentWindow && typeof el.contentWindow.postMessage === 'function' ) {
 | 
			
		||||
					el.contentWindow.postMessage( '{"event":"command","func":"pauseVideo","args":""}', '*' );
 | 
			
		||||
				}
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			// Vimeo postMessage API
 | 
			
		||||
			queryAll( element, 'iframe[src*="player.vimeo.com/"]' ).forEach( el => {
 | 
			
		||||
				if( !el.hasAttribute( 'data-ignore' ) && el.contentWindow && typeof el.contentWindow.postMessage === 'function' ) {
 | 
			
		||||
					el.contentWindow.postMessage( '{"method":"pause"}', '*' );
 | 
			
		||||
				}
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			if( options.unloadIframes === true ) {
 | 
			
		||||
				// Unload lazy-loaded iframes
 | 
			
		||||
				queryAll( element, 'iframe[data-src]' ).forEach( el => {
 | 
			
		||||
					// Only removing the src doesn't actually unload the frame
 | 
			
		||||
					// in all browsers (Firefox) so we set it to blank first
 | 
			
		||||
					el.setAttribute( 'src', 'about:blank' );
 | 
			
		||||
					el.removeAttribute( 'src' );
 | 
			
		||||
				} );
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										132
									
								
								js/controllers/slidenumber.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								js/controllers/slidenumber.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,132 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Handles the display of reveal.js' optional slide number.
 | 
			
		||||
 */
 | 
			
		||||
export default class SlideNumber {
 | 
			
		||||
 | 
			
		||||
	constructor( Reveal ) {
 | 
			
		||||
 | 
			
		||||
		this.Reveal = Reveal;
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render() {
 | 
			
		||||
 | 
			
		||||
		this.element = document.createElement( 'div' );
 | 
			
		||||
		this.element.className = 'slide-number';
 | 
			
		||||
		this.Reveal.getRevealElement().appendChild( this.element );
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Called when the reveal.js config is updated.
 | 
			
		||||
	 */
 | 
			
		||||
	configure( config, oldConfig ) {
 | 
			
		||||
 | 
			
		||||
		let slideNumberDisplay = 'none';
 | 
			
		||||
		if( config.slideNumber && !this.Reveal.isPrintingPDF() ) {
 | 
			
		||||
			if( config.showSlideNumber === 'all' ) {
 | 
			
		||||
				slideNumberDisplay = 'block';
 | 
			
		||||
			}
 | 
			
		||||
			else if( config.showSlideNumber === 'speaker' && this.Reveal.isSpeakerNotes() ) {
 | 
			
		||||
				slideNumberDisplay = 'block';
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		this.element.style.display = slideNumberDisplay;
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Updates the slide number to match the current slide.
 | 
			
		||||
	 */
 | 
			
		||||
	update() {
 | 
			
		||||
 | 
			
		||||
		// Update slide number if enabled
 | 
			
		||||
		if( this.Reveal.getConfig().slideNumber && this.element ) {
 | 
			
		||||
			this.element.innerHTML = this.getSlideNumber();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Returns the HTML string corresponding to the current slide
 | 
			
		||||
	 * number, including formatting.
 | 
			
		||||
	 */
 | 
			
		||||
	getSlideNumber( slide = this.Reveal.getCurrentSlide() ) {
 | 
			
		||||
 | 
			
		||||
		let config = this.Reveal.getConfig();
 | 
			
		||||
		let value;
 | 
			
		||||
		let format = 'h.v';
 | 
			
		||||
 | 
			
		||||
		if ( typeof config.slideNumber === 'function' ) {
 | 
			
		||||
			value = config.slideNumber( slide );
 | 
			
		||||
		} else {
 | 
			
		||||
			// Check if a custom number format is available
 | 
			
		||||
			if( typeof config.slideNumber === 'string' ) {
 | 
			
		||||
				format = config.slideNumber;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// If there are ONLY vertical slides in this deck, always use
 | 
			
		||||
			// a flattened slide number
 | 
			
		||||
			if( !/c/.test( format ) && this.Reveal.getHorizontalSlides().length === 1 ) {
 | 
			
		||||
				format = 'c';
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// Offset the current slide number by 1 to make it 1-indexed
 | 
			
		||||
			let horizontalOffset = slide && slide.dataset.visibility === 'uncounted' ? 0 : 1;
 | 
			
		||||
 | 
			
		||||
			value = [];
 | 
			
		||||
			switch( format ) {
 | 
			
		||||
				case 'c':
 | 
			
		||||
					value.push( this.Reveal.getSlidePastCount( slide ) + horizontalOffset );
 | 
			
		||||
					break;
 | 
			
		||||
				case 'c/t':
 | 
			
		||||
					value.push( this.Reveal.getSlidePastCount( slide ) + horizontalOffset, '/', this.Reveal.getTotalSlides() );
 | 
			
		||||
					break;
 | 
			
		||||
				default:
 | 
			
		||||
					let indices = this.Reveal.getIndices( slide );
 | 
			
		||||
					value.push( indices.h + horizontalOffset );
 | 
			
		||||
					let sep = format === 'h/v' ? '/' : '.';
 | 
			
		||||
					if( this.Reveal.isVerticalSlide( slide ) ) value.push( sep, indices.v + 1 );
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		let url = '#' + this.Reveal.location.getHash( slide );
 | 
			
		||||
		return this.formatNumber( value[0], value[1], value[2], url );
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Applies HTML formatting to a slide number before it's
 | 
			
		||||
	 * written to the DOM.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param {number} a Current slide
 | 
			
		||||
	 * @param {string} delimiter Character to separate slide numbers
 | 
			
		||||
	 * @param {(number|*)} b Total slides
 | 
			
		||||
	 * @param {HTMLElement} [url='#'+locationHash()] The url to link to
 | 
			
		||||
	 * @return {string} HTML string fragment
 | 
			
		||||
	 */
 | 
			
		||||
	formatNumber( a, delimiter, b, url = '#' + this.Reveal.location.getHash() ) {
 | 
			
		||||
 | 
			
		||||
		if( typeof b === 'number' && !isNaN( b ) ) {
 | 
			
		||||
			return  `<a href="${url}">
 | 
			
		||||
					<span class="slide-number-a">${a}</span>
 | 
			
		||||
					<span class="slide-number-delimiter">${delimiter}</span>
 | 
			
		||||
					<span class="slide-number-b">${b}</span>
 | 
			
		||||
					</a>`;
 | 
			
		||||
		}
 | 
			
		||||
		else {
 | 
			
		||||
			return `<a href="${url}">
 | 
			
		||||
					<span class="slide-number-a">${a}</span>
 | 
			
		||||
					</a>`;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	destroy() {
 | 
			
		||||
 | 
			
		||||
		this.element.remove();
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										263
									
								
								js/controllers/touch.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										263
									
								
								js/controllers/touch.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,263 @@
 | 
			
		||||
import { isAndroid } from '../utils/device.js'
 | 
			
		||||
import { matches } from '../utils/util.js'
 | 
			
		||||
 | 
			
		||||
const SWIPE_THRESHOLD = 40;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Controls all touch interactions and navigations for
 | 
			
		||||
 * a presentation.
 | 
			
		||||
 */
 | 
			
		||||
export default class Touch {
 | 
			
		||||
 | 
			
		||||
	constructor( Reveal ) {
 | 
			
		||||
 | 
			
		||||
		this.Reveal = Reveal;
 | 
			
		||||
 | 
			
		||||
		// Holds information about the currently ongoing touch interaction
 | 
			
		||||
		this.touchStartX = 0;
 | 
			
		||||
		this.touchStartY = 0;
 | 
			
		||||
		this.touchStartCount = 0;
 | 
			
		||||
		this.touchCaptured = false;
 | 
			
		||||
 | 
			
		||||
		this.onPointerDown = this.onPointerDown.bind( this );
 | 
			
		||||
		this.onPointerMove = this.onPointerMove.bind( this );
 | 
			
		||||
		this.onPointerUp = this.onPointerUp.bind( this );
 | 
			
		||||
		this.onTouchStart = this.onTouchStart.bind( this );
 | 
			
		||||
		this.onTouchMove = this.onTouchMove.bind( this );
 | 
			
		||||
		this.onTouchEnd = this.onTouchEnd.bind( this );
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 *
 | 
			
		||||
	 */
 | 
			
		||||
	bind() {
 | 
			
		||||
 | 
			
		||||
		let revealElement = this.Reveal.getRevealElement();
 | 
			
		||||
 | 
			
		||||
		if( 'onpointerdown' in window ) {
 | 
			
		||||
			// Use W3C pointer events
 | 
			
		||||
			revealElement.addEventListener( 'pointerdown', this.onPointerDown, false );
 | 
			
		||||
			revealElement.addEventListener( 'pointermove', this.onPointerMove, false );
 | 
			
		||||
			revealElement.addEventListener( 'pointerup', this.onPointerUp, false );
 | 
			
		||||
		}
 | 
			
		||||
		else if( window.navigator.msPointerEnabled ) {
 | 
			
		||||
			// IE 10 uses prefixed version of pointer events
 | 
			
		||||
			revealElement.addEventListener( 'MSPointerDown', this.onPointerDown, false );
 | 
			
		||||
			revealElement.addEventListener( 'MSPointerMove', this.onPointerMove, false );
 | 
			
		||||
			revealElement.addEventListener( 'MSPointerUp', this.onPointerUp, false );
 | 
			
		||||
		}
 | 
			
		||||
		else {
 | 
			
		||||
			// Fall back to touch events
 | 
			
		||||
			revealElement.addEventListener( 'touchstart', this.onTouchStart, false );
 | 
			
		||||
			revealElement.addEventListener( 'touchmove', this.onTouchMove, false );
 | 
			
		||||
			revealElement.addEventListener( 'touchend', this.onTouchEnd, false );
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 *
 | 
			
		||||
	 */
 | 
			
		||||
	unbind() {
 | 
			
		||||
 | 
			
		||||
		let revealElement = this.Reveal.getRevealElement();
 | 
			
		||||
 | 
			
		||||
		revealElement.removeEventListener( 'pointerdown', this.onPointerDown, false );
 | 
			
		||||
		revealElement.removeEventListener( 'pointermove', this.onPointerMove, false );
 | 
			
		||||
		revealElement.removeEventListener( 'pointerup', this.onPointerUp, false );
 | 
			
		||||
 | 
			
		||||
		revealElement.removeEventListener( 'MSPointerDown', this.onPointerDown, false );
 | 
			
		||||
		revealElement.removeEventListener( 'MSPointerMove', this.onPointerMove, false );
 | 
			
		||||
		revealElement.removeEventListener( 'MSPointerUp', this.onPointerUp, false );
 | 
			
		||||
 | 
			
		||||
		revealElement.removeEventListener( 'touchstart', this.onTouchStart, false );
 | 
			
		||||
		revealElement.removeEventListener( 'touchmove', this.onTouchMove, false );
 | 
			
		||||
		revealElement.removeEventListener( 'touchend', this.onTouchEnd, false );
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Checks if the target element prevents the triggering of
 | 
			
		||||
	 * swipe navigation.
 | 
			
		||||
	 */
 | 
			
		||||
	isSwipePrevented( target ) {
 | 
			
		||||
 | 
			
		||||
		// Prevent accidental swipes when scrubbing timelines
 | 
			
		||||
		if( matches( target, 'video, audio' ) ) return true;
 | 
			
		||||
 | 
			
		||||
		while( target && typeof target.hasAttribute === 'function' ) {
 | 
			
		||||
			if( target.hasAttribute( 'data-prevent-swipe' ) ) return true;
 | 
			
		||||
			target = target.parentNode;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return false;
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Handler for the 'touchstart' event, enables support for
 | 
			
		||||
	 * swipe and pinch gestures.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param {object} event
 | 
			
		||||
	 */
 | 
			
		||||
	onTouchStart( event ) {
 | 
			
		||||
 | 
			
		||||
		if( this.isSwipePrevented( event.target ) ) return true;
 | 
			
		||||
 | 
			
		||||
		this.touchStartX = event.touches[0].clientX;
 | 
			
		||||
		this.touchStartY = event.touches[0].clientY;
 | 
			
		||||
		this.touchStartCount = event.touches.length;
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Handler for the 'touchmove' event.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param {object} event
 | 
			
		||||
	 */
 | 
			
		||||
	onTouchMove( event ) {
 | 
			
		||||
 | 
			
		||||
		if( this.isSwipePrevented( event.target ) ) return true;
 | 
			
		||||
 | 
			
		||||
		let config = this.Reveal.getConfig();
 | 
			
		||||
 | 
			
		||||
		// Each touch should only trigger one action
 | 
			
		||||
		if( !this.touchCaptured ) {
 | 
			
		||||
			this.Reveal.onUserInput( event );
 | 
			
		||||
 | 
			
		||||
			let currentX = event.touches[0].clientX;
 | 
			
		||||
			let currentY = event.touches[0].clientY;
 | 
			
		||||
 | 
			
		||||
			// There was only one touch point, look for a swipe
 | 
			
		||||
			if( event.touches.length === 1 && this.touchStartCount !== 2 ) {
 | 
			
		||||
 | 
			
		||||
				let availableRoutes = this.Reveal.availableRoutes({ includeFragments: true });
 | 
			
		||||
 | 
			
		||||
				let deltaX = currentX - this.touchStartX,
 | 
			
		||||
					deltaY = currentY - this.touchStartY;
 | 
			
		||||
 | 
			
		||||
				if( deltaX > SWIPE_THRESHOLD && Math.abs( deltaX ) > Math.abs( deltaY ) ) {
 | 
			
		||||
					this.touchCaptured = true;
 | 
			
		||||
					if( config.navigationMode === 'linear' ) {
 | 
			
		||||
						if( config.rtl ) {
 | 
			
		||||
							this.Reveal.next();
 | 
			
		||||
						}
 | 
			
		||||
						else {
 | 
			
		||||
							this.Reveal.prev();
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
					else {
 | 
			
		||||
						this.Reveal.left();
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
				else if( deltaX < -SWIPE_THRESHOLD && Math.abs( deltaX ) > Math.abs( deltaY ) ) {
 | 
			
		||||
					this.touchCaptured = true;
 | 
			
		||||
					if( config.navigationMode === 'linear' ) {
 | 
			
		||||
						if( config.rtl ) {
 | 
			
		||||
							this.Reveal.prev();
 | 
			
		||||
						}
 | 
			
		||||
						else {
 | 
			
		||||
							this.Reveal.next();
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
					else {
 | 
			
		||||
						this.Reveal.right();
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
				else if( deltaY > SWIPE_THRESHOLD && availableRoutes.up ) {
 | 
			
		||||
					this.touchCaptured = true;
 | 
			
		||||
					if( config.navigationMode === 'linear' ) {
 | 
			
		||||
						this.Reveal.prev();
 | 
			
		||||
					}
 | 
			
		||||
					else {
 | 
			
		||||
						this.Reveal.up();
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
				else if( deltaY < -SWIPE_THRESHOLD && availableRoutes.down ) {
 | 
			
		||||
					this.touchCaptured = true;
 | 
			
		||||
					if( config.navigationMode === 'linear' ) {
 | 
			
		||||
						this.Reveal.next();
 | 
			
		||||
					}
 | 
			
		||||
					else {
 | 
			
		||||
						this.Reveal.down();
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				// If we're embedded, only block touch events if they have
 | 
			
		||||
				// triggered an action
 | 
			
		||||
				if( config.embedded ) {
 | 
			
		||||
					if( this.touchCaptured || this.Reveal.isVerticalSlide() ) {
 | 
			
		||||
						event.preventDefault();
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
				// Not embedded? Block them all to avoid needless tossing
 | 
			
		||||
				// around of the viewport in iOS
 | 
			
		||||
				else {
 | 
			
		||||
					event.preventDefault();
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		// There's a bug with swiping on some Android devices unless
 | 
			
		||||
		// the default action is always prevented
 | 
			
		||||
		else if( isAndroid ) {
 | 
			
		||||
			event.preventDefault();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Handler for the 'touchend' event.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param {object} event
 | 
			
		||||
	 */
 | 
			
		||||
	onTouchEnd( event ) {
 | 
			
		||||
 | 
			
		||||
		this.touchCaptured = false;
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Convert pointer down to touch start.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param {object} event
 | 
			
		||||
	 */
 | 
			
		||||
	onPointerDown( event ) {
 | 
			
		||||
 | 
			
		||||
		if( event.pointerType === event.MSPOINTER_TYPE_TOUCH || event.pointerType === "touch" ) {
 | 
			
		||||
			event.touches = [{ clientX: event.clientX, clientY: event.clientY }];
 | 
			
		||||
			this.onTouchStart( event );
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Convert pointer move to touch move.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param {object} event
 | 
			
		||||
	 */
 | 
			
		||||
	onPointerMove( event ) {
 | 
			
		||||
 | 
			
		||||
		if( event.pointerType === event.MSPOINTER_TYPE_TOUCH || event.pointerType === "touch" )  {
 | 
			
		||||
			event.touches = [{ clientX: event.clientX, clientY: event.clientY }];
 | 
			
		||||
			this.onTouchMove( event );
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Convert pointer up to touch end.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param {object} event
 | 
			
		||||
	 */
 | 
			
		||||
	onPointerUp( event ) {
 | 
			
		||||
 | 
			
		||||
		if( event.pointerType === event.MSPOINTER_TYPE_TOUCH || event.pointerType === "touch" )  {
 | 
			
		||||
			event.touches = [{ clientX: event.clientX, clientY: event.clientY }];
 | 
			
		||||
			this.onTouchEnd( event );
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										58
									
								
								js/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								js/index.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,58 @@
 | 
			
		||||
import Deck, { VERSION } from './reveal.js'
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Expose the Reveal class to the window. To create a
 | 
			
		||||
 * new instance:
 | 
			
		||||
 * let deck = new Reveal( document.querySelector( '.reveal' ), {
 | 
			
		||||
 *   controls: false
 | 
			
		||||
 * } );
 | 
			
		||||
 * deck.initialize().then(() => {
 | 
			
		||||
 *   // reveal.js is ready
 | 
			
		||||
 * });
 | 
			
		||||
 */
 | 
			
		||||
let Reveal = Deck;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * The below is a thin shell that mimics the pre 4.0
 | 
			
		||||
 * reveal.js API and ensures backwards compatibility.
 | 
			
		||||
 * This API only allows for one Reveal instance per
 | 
			
		||||
 * page, whereas the new API above lets you run many
 | 
			
		||||
 * presentations on the same page.
 | 
			
		||||
 *
 | 
			
		||||
 * Reveal.initialize( { controls: false } ).then(() => {
 | 
			
		||||
 *   // reveal.js is ready
 | 
			
		||||
 * });
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
let enqueuedAPICalls = [];
 | 
			
		||||
 | 
			
		||||
Reveal.initialize = options => {
 | 
			
		||||
 | 
			
		||||
	// Create our singleton reveal.js instance
 | 
			
		||||
	Object.assign( Reveal, new Deck( document.querySelector( '.reveal' ), options ) );
 | 
			
		||||
 | 
			
		||||
	// Invoke any enqueued API calls
 | 
			
		||||
	enqueuedAPICalls.map( method => method( Reveal ) );
 | 
			
		||||
 | 
			
		||||
	return Reveal.initialize();
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * The pre 4.0 API let you add event listener before
 | 
			
		||||
 * initializing. We maintain the same behavior by
 | 
			
		||||
 * queuing up premature API calls and invoking all
 | 
			
		||||
 * of them when Reveal.initialize is called.
 | 
			
		||||
 */
 | 
			
		||||
[ 'configure', 'on', 'off', 'addEventListener', 'removeEventListener', 'registerPlugin' ].forEach( method => {
 | 
			
		||||
	Reveal[method] = ( ...args ) => {
 | 
			
		||||
		enqueuedAPICalls.push( deck => deck[method].call( null, ...args ) );
 | 
			
		||||
	}
 | 
			
		||||
} );
 | 
			
		||||
 | 
			
		||||
Reveal.isReady = () => false;
 | 
			
		||||
 | 
			
		||||
Reveal.VERSION = VERSION;
 | 
			
		||||
 | 
			
		||||
export default Reveal;
 | 
			
		||||
							
								
								
									
										2810
									
								
								js/reveal.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2810
									
								
								js/reveal.js
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										77
									
								
								js/utils/color.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								js/utils/color.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,77 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Converts various color input formats to an {r:0,g:0,b:0} object.
 | 
			
		||||
 *
 | 
			
		||||
 * @param {string} color The string representation of a color
 | 
			
		||||
 * @example
 | 
			
		||||
 * colorToRgb('#000');
 | 
			
		||||
 * @example
 | 
			
		||||
 * colorToRgb('#000000');
 | 
			
		||||
 * @example
 | 
			
		||||
 * colorToRgb('rgb(0,0,0)');
 | 
			
		||||
 * @example
 | 
			
		||||
 * colorToRgb('rgba(0,0,0)');
 | 
			
		||||
 *
 | 
			
		||||
 * @return {{r: number, g: number, b: number, [a]: number}|null}
 | 
			
		||||
 */
 | 
			
		||||
export const colorToRgb = ( color ) => {
 | 
			
		||||
 | 
			
		||||
	let hex3 = color.match( /^#([0-9a-f]{3})$/i );
 | 
			
		||||
	if( hex3 && hex3[1] ) {
 | 
			
		||||
		hex3 = hex3[1];
 | 
			
		||||
		return {
 | 
			
		||||
			r: parseInt( hex3.charAt( 0 ), 16 ) * 0x11,
 | 
			
		||||
			g: parseInt( hex3.charAt( 1 ), 16 ) * 0x11,
 | 
			
		||||
			b: parseInt( hex3.charAt( 2 ), 16 ) * 0x11
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	let hex6 = color.match( /^#([0-9a-f]{6})$/i );
 | 
			
		||||
	if( hex6 && hex6[1] ) {
 | 
			
		||||
		hex6 = hex6[1];
 | 
			
		||||
		return {
 | 
			
		||||
			r: parseInt( hex6.slice( 0, 2 ), 16 ),
 | 
			
		||||
			g: parseInt( hex6.slice( 2, 4 ), 16 ),
 | 
			
		||||
			b: parseInt( hex6.slice( 4, 6 ), 16 )
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	let rgb = color.match( /^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/i );
 | 
			
		||||
	if( rgb ) {
 | 
			
		||||
		return {
 | 
			
		||||
			r: parseInt( rgb[1], 10 ),
 | 
			
		||||
			g: parseInt( rgb[2], 10 ),
 | 
			
		||||
			b: parseInt( rgb[3], 10 )
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	let rgba = color.match( /^rgba\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\,\s*([\d]+|[\d]*.[\d]+)\s*\)$/i );
 | 
			
		||||
	if( rgba ) {
 | 
			
		||||
		return {
 | 
			
		||||
			r: parseInt( rgba[1], 10 ),
 | 
			
		||||
			g: parseInt( rgba[2], 10 ),
 | 
			
		||||
			b: parseInt( rgba[3], 10 ),
 | 
			
		||||
			a: parseFloat( rgba[4] )
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return null;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Calculates brightness on a scale of 0-255.
 | 
			
		||||
 *
 | 
			
		||||
 * @param {string} color See colorToRgb for supported formats.
 | 
			
		||||
 * @see {@link colorToRgb}
 | 
			
		||||
 */
 | 
			
		||||
export const colorBrightness = ( color ) => {
 | 
			
		||||
 | 
			
		||||
	if( typeof color === 'string' ) color = colorToRgb( color );
 | 
			
		||||
 | 
			
		||||
	if( color ) {
 | 
			
		||||
		return ( color.r * 299 + color.g * 587 + color.b * 114 ) / 1000;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return null;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										10
									
								
								js/utils/constants.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								js/utils/constants.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
			
		||||
 | 
			
		||||
export const SLIDES_SELECTOR = '.slides section';
 | 
			
		||||
export const HORIZONTAL_SLIDES_SELECTOR = '.slides>section';
 | 
			
		||||
export const VERTICAL_SLIDES_SELECTOR = '.slides>section.present>section';
 | 
			
		||||
 | 
			
		||||
// Methods that may not be invoked via the postMessage API
 | 
			
		||||
export const POST_MESSAGE_METHOD_BLACKLIST = /registerPlugin|registerKeyboardShortcut|addKeyBinding|addEventListener/;
 | 
			
		||||
 | 
			
		||||
// Regex for retrieving the fragment style from a class attribute
 | 
			
		||||
export const FRAGMENT_STYLE_REGEX = /fade-(down|up|right|left|out|in-then-out|in-then-semi-out)|semi-fade-out|current-visible|shrink|grow/;
 | 
			
		||||
							
								
								
									
										8
									
								
								js/utils/device.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								js/utils/device.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,8 @@
 | 
			
		||||
const UA = navigator.userAgent;
 | 
			
		||||
 | 
			
		||||
export const isMobile = /(iphone|ipod|ipad|android)/gi.test( UA ) ||
 | 
			
		||||
						( navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1 ); // iPadOS
 | 
			
		||||
 | 
			
		||||
export const isChrome = /chrome/i.test( UA ) && !/edge/i.test( UA );
 | 
			
		||||
 | 
			
		||||
export const isAndroid = /android/gi.test( UA );
 | 
			
		||||
							
								
								
									
										46
									
								
								js/utils/loader.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								js/utils/loader.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,46 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Loads a JavaScript file from the given URL and executes it.
 | 
			
		||||
 *
 | 
			
		||||
 * @param {string} url Address of the .js file to load
 | 
			
		||||
 * @param {function} callback Method to invoke when the script
 | 
			
		||||
 * has loaded and executed
 | 
			
		||||
 */
 | 
			
		||||
export const loadScript = ( url, callback ) => {
 | 
			
		||||
 | 
			
		||||
	const script = document.createElement( 'script' );
 | 
			
		||||
	script.type = 'text/javascript';
 | 
			
		||||
	script.async = false;
 | 
			
		||||
	script.defer = false;
 | 
			
		||||
	script.src = url;
 | 
			
		||||
 | 
			
		||||
	if( typeof callback === 'function' ) {
 | 
			
		||||
 | 
			
		||||
		// Success callback
 | 
			
		||||
		script.onload = script.onreadystatechange = event => {
 | 
			
		||||
			if( event.type === 'load' || /loaded|complete/.test( script.readyState ) ) {
 | 
			
		||||
 | 
			
		||||
				// Kill event listeners
 | 
			
		||||
				script.onload = script.onreadystatechange = script.onerror = null;
 | 
			
		||||
 | 
			
		||||
				callback();
 | 
			
		||||
 | 
			
		||||
			}
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		// Error callback
 | 
			
		||||
		script.onerror = err => {
 | 
			
		||||
 | 
			
		||||
			// Kill event listeners
 | 
			
		||||
			script.onload = script.onreadystatechange = script.onerror = null;
 | 
			
		||||
 | 
			
		||||
			callback( new Error( 'Failed loading script: ' + script.src + '\n' + err ) );
 | 
			
		||||
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Append the script at the end of <head>
 | 
			
		||||
	const head = document.querySelector( 'head' );
 | 
			
		||||
	head.insertBefore( script, head.lastChild );
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										297
									
								
								js/utils/util.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										297
									
								
								js/utils/util.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,297 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Extend object a with the properties of object b.
 | 
			
		||||
 * If there's a conflict, object b takes precedence.
 | 
			
		||||
 *
 | 
			
		||||
 * @param {object} a
 | 
			
		||||
 * @param {object} b
 | 
			
		||||
 */
 | 
			
		||||
export const extend = ( a, b ) => {
 | 
			
		||||
 | 
			
		||||
	for( let i in b ) {
 | 
			
		||||
		a[ i ] = b[ i ];
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return a;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * querySelectorAll but returns an Array.
 | 
			
		||||
 */
 | 
			
		||||
export const queryAll = ( el, selector ) => {
 | 
			
		||||
 | 
			
		||||
	return Array.from( el.querySelectorAll( selector ) );
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * classList.toggle() with cross browser support
 | 
			
		||||
 */
 | 
			
		||||
export const toggleClass = ( el, className, value ) => {
 | 
			
		||||
	if( value ) {
 | 
			
		||||
		el.classList.add( className );
 | 
			
		||||
	}
 | 
			
		||||
	else {
 | 
			
		||||
		el.classList.remove( className );
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Utility for deserializing a value.
 | 
			
		||||
 *
 | 
			
		||||
 * @param {*} value
 | 
			
		||||
 * @return {*}
 | 
			
		||||
 */
 | 
			
		||||
export const deserialize = ( value ) => {
 | 
			
		||||
 | 
			
		||||
	if( typeof value === 'string' ) {
 | 
			
		||||
		if( value === 'null' ) return null;
 | 
			
		||||
		else if( value === 'true' ) return true;
 | 
			
		||||
		else if( value === 'false' ) return false;
 | 
			
		||||
		else if( value.match( /^-?[\d\.]+$/ ) ) return parseFloat( value );
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return value;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Measures the distance in pixels between point a
 | 
			
		||||
 * and point b.
 | 
			
		||||
 *
 | 
			
		||||
 * @param {object} a point with x/y properties
 | 
			
		||||
 * @param {object} b point with x/y properties
 | 
			
		||||
 *
 | 
			
		||||
 * @return {number}
 | 
			
		||||
 */
 | 
			
		||||
export const distanceBetween = ( a, b ) => {
 | 
			
		||||
 | 
			
		||||
	let dx = a.x - b.x,
 | 
			
		||||
		dy = a.y - b.y;
 | 
			
		||||
 | 
			
		||||
	return Math.sqrt( dx*dx + dy*dy );
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Applies a CSS transform to the target element.
 | 
			
		||||
 *
 | 
			
		||||
 * @param {HTMLElement} element
 | 
			
		||||
 * @param {string} transform
 | 
			
		||||
 */
 | 
			
		||||
export const transformElement = ( element, transform ) => {
 | 
			
		||||
 | 
			
		||||
	element.style.transform = transform;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Element.matches with IE support.
 | 
			
		||||
 *
 | 
			
		||||
 * @param {HTMLElement} target The element to match
 | 
			
		||||
 * @param {String} selector The CSS selector to match
 | 
			
		||||
 * the element against
 | 
			
		||||
 *
 | 
			
		||||
 * @return {Boolean}
 | 
			
		||||
 */
 | 
			
		||||
export const matches = ( target, selector ) => {
 | 
			
		||||
 | 
			
		||||
	let matchesMethod = target.matches || target.matchesSelector || target.msMatchesSelector;
 | 
			
		||||
 | 
			
		||||
	return !!( matchesMethod && matchesMethod.call( target, selector ) );
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Find the closest parent that matches the given
 | 
			
		||||
 * selector.
 | 
			
		||||
 *
 | 
			
		||||
 * @param {HTMLElement} target The child element
 | 
			
		||||
 * @param {String} selector The CSS selector to match
 | 
			
		||||
 * the parents against
 | 
			
		||||
 *
 | 
			
		||||
 * @return {HTMLElement} The matched parent or null
 | 
			
		||||
 * if no matching parent was found
 | 
			
		||||
 */
 | 
			
		||||
export const closest = ( target, selector ) => {
 | 
			
		||||
 | 
			
		||||
	// Native Element.closest
 | 
			
		||||
	if( typeof target.closest === 'function' ) {
 | 
			
		||||
		return target.closest( selector );
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Polyfill
 | 
			
		||||
	while( target ) {
 | 
			
		||||
		if( matches( target, selector ) ) {
 | 
			
		||||
			return target;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Keep searching
 | 
			
		||||
		target = target.parentNode;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return null;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Handling the fullscreen functionality via the fullscreen API
 | 
			
		||||
 *
 | 
			
		||||
 * @see http://fullscreen.spec.whatwg.org/
 | 
			
		||||
 * @see https://developer.mozilla.org/en-US/docs/DOM/Using_fullscreen_mode
 | 
			
		||||
 */
 | 
			
		||||
export const enterFullscreen = element => {
 | 
			
		||||
 | 
			
		||||
	element = element || document.documentElement;
 | 
			
		||||
 | 
			
		||||
	// Check which implementation is available
 | 
			
		||||
	let requestMethod = element.requestFullscreen ||
 | 
			
		||||
						element.webkitRequestFullscreen ||
 | 
			
		||||
						element.webkitRequestFullScreen ||
 | 
			
		||||
						element.mozRequestFullScreen ||
 | 
			
		||||
						element.msRequestFullscreen;
 | 
			
		||||
 | 
			
		||||
	if( requestMethod ) {
 | 
			
		||||
		requestMethod.apply( element );
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Creates an HTML element and returns a reference to it.
 | 
			
		||||
 * If the element already exists the existing instance will
 | 
			
		||||
 * be returned.
 | 
			
		||||
 *
 | 
			
		||||
 * @param {HTMLElement} container
 | 
			
		||||
 * @param {string} tagname
 | 
			
		||||
 * @param {string} classname
 | 
			
		||||
 * @param {string} innerHTML
 | 
			
		||||
 *
 | 
			
		||||
 * @return {HTMLElement}
 | 
			
		||||
 */
 | 
			
		||||
export const createSingletonNode = ( container, tagname, classname, innerHTML='' ) => {
 | 
			
		||||
 | 
			
		||||
	// Find all nodes matching the description
 | 
			
		||||
	let nodes = container.querySelectorAll( '.' + classname );
 | 
			
		||||
 | 
			
		||||
	// Check all matches to find one which is a direct child of
 | 
			
		||||
	// the specified container
 | 
			
		||||
	for( let i = 0; i < nodes.length; i++ ) {
 | 
			
		||||
		let testNode = nodes[i];
 | 
			
		||||
		if( testNode.parentNode === container ) {
 | 
			
		||||
			return testNode;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// If no node was found, create it now
 | 
			
		||||
	let node = document.createElement( tagname );
 | 
			
		||||
	node.className = classname;
 | 
			
		||||
	node.innerHTML = innerHTML;
 | 
			
		||||
	container.appendChild( node );
 | 
			
		||||
 | 
			
		||||
	return node;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Injects the given CSS styles into the DOM.
 | 
			
		||||
 *
 | 
			
		||||
 * @param {string} value
 | 
			
		||||
 */
 | 
			
		||||
export const createStyleSheet = ( value ) => {
 | 
			
		||||
 | 
			
		||||
	let tag = document.createElement( 'style' );
 | 
			
		||||
	tag.type = 'text/css';
 | 
			
		||||
 | 
			
		||||
	if( value && value.length > 0 ) {
 | 
			
		||||
		if( tag.styleSheet ) {
 | 
			
		||||
			tag.styleSheet.cssText = value;
 | 
			
		||||
		}
 | 
			
		||||
		else {
 | 
			
		||||
			tag.appendChild( document.createTextNode( value ) );
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	document.head.appendChild( tag );
 | 
			
		||||
 | 
			
		||||
	return tag;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Returns a key:value hash of all query params.
 | 
			
		||||
 */
 | 
			
		||||
export const getQueryHash = () => {
 | 
			
		||||
 | 
			
		||||
	let query = {};
 | 
			
		||||
 | 
			
		||||
	location.search.replace( /[A-Z0-9]+?=([\w\.%-]*)/gi, a => {
 | 
			
		||||
		query[ a.split( '=' ).shift() ] = a.split( '=' ).pop();
 | 
			
		||||
	} );
 | 
			
		||||
 | 
			
		||||
	// Basic deserialization
 | 
			
		||||
	for( let i in query ) {
 | 
			
		||||
		let value = query[ i ];
 | 
			
		||||
 | 
			
		||||
		query[ i ] = deserialize( unescape( value ) );
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Do not accept new dependencies via query config to avoid
 | 
			
		||||
	// the potential of malicious script injection
 | 
			
		||||
	if( typeof query['dependencies'] !== 'undefined' ) delete query['dependencies'];
 | 
			
		||||
 | 
			
		||||
	return query;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Returns the remaining height within the parent of the
 | 
			
		||||
 * target element.
 | 
			
		||||
 *
 | 
			
		||||
 * remaining height = [ configured parent height ] - [ current parent height ]
 | 
			
		||||
 *
 | 
			
		||||
 * @param {HTMLElement} element
 | 
			
		||||
 * @param {number} [height]
 | 
			
		||||
 */
 | 
			
		||||
export const getRemainingHeight = ( element, height = 0 ) => {
 | 
			
		||||
 | 
			
		||||
	if( element ) {
 | 
			
		||||
		let newHeight, oldHeight = element.style.height;
 | 
			
		||||
 | 
			
		||||
		// Change the .stretch element height to 0 in order find the height of all
 | 
			
		||||
		// the other elements
 | 
			
		||||
		element.style.height = '0px';
 | 
			
		||||
 | 
			
		||||
		// In Overview mode, the parent (.slide) height is set of 700px.
 | 
			
		||||
		// Restore it temporarily to its natural height.
 | 
			
		||||
		element.parentNode.style.height = 'auto';
 | 
			
		||||
 | 
			
		||||
		newHeight = height - element.parentNode.offsetHeight;
 | 
			
		||||
 | 
			
		||||
		// Restore the old height, just in case
 | 
			
		||||
		element.style.height = oldHeight + 'px';
 | 
			
		||||
 | 
			
		||||
		// Clear the parent (.slide) height. .removeProperty works in IE9+
 | 
			
		||||
		element.parentNode.style.removeProperty('height');
 | 
			
		||||
 | 
			
		||||
		return newHeight;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return height;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const fileExtensionToMimeMap = {
 | 
			
		||||
	'mp4': 'video/mp4',
 | 
			
		||||
	'm4a': 'video/mp4',
 | 
			
		||||
	'ogv': 'video/ogg',
 | 
			
		||||
	'mpeg': 'video/mpeg',
 | 
			
		||||
	'webm': 'video/webm'
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Guess the MIME type for common file formats.
 | 
			
		||||
 */
 | 
			
		||||
export const getMimeTypeFromFile = ( filename='' ) => {
 | 
			
		||||
	return fileExtensionToMimeMap[filename.split('.').pop()]
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user