Merge branch 'dev' into dev_importBundledPlugins
This commit is contained in:
		
							
								
								
									
										2
									
								
								dist/reveal.min.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								dist/reveal.min.js
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										548
									
								
								js/controllers/autoanimate.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										548
									
								
								js/controllers/autoanimate.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,548 @@ | |||||||
|  | import { extend, toArray, createStyleSheet } from '../utils/util.js' | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Automatically animates matching elements across | ||||||
|  |  * slides with the [data-auto-animate] attribute. | ||||||
|  |  */ | ||||||
|  | export default class AutoAnimate { | ||||||
|  |  | ||||||
|  | 	constructor( Reveal ) { | ||||||
|  |  | ||||||
|  | 		this.Reveal = Reveal; | ||||||
|  |  | ||||||
|  | 		// Counter used to generate unique IDs for auto-animated elements | ||||||
|  | 		this.autoAnimateCounter = 0; | ||||||
|  |  | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	/** | ||||||
|  | 	 * Runs an auto-animation between the given slides. | ||||||
|  | 	 * | ||||||
|  | 	 * @param  {HTMLElement} fromSlide | ||||||
|  | 	 * @param  {HTMLElement} toSlide | ||||||
|  | 	 */ | ||||||
|  | 	run( fromSlide, toSlide ) { | ||||||
|  |  | ||||||
|  | 		// Clean up after prior animations | ||||||
|  | 		this.reset(); | ||||||
|  |  | ||||||
|  | 		// Ensure that both slides are auto-animate targets | ||||||
|  | 		if( fromSlide.hasAttribute( 'data-auto-animate' ) && toSlide.hasAttribute( 'data-auto-animate' ) ) { | ||||||
|  |  | ||||||
|  | 			// 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'; | ||||||
|  |  | ||||||
|  | 			// Inject our auto-animate styles for this transition | ||||||
|  | 			let css = this.getAutoAnimatableElements( fromSlide, toSlide ).map( elements => { | ||||||
|  | 				return this.getAutoAnimateCSS( elements.from, elements.to, elements.options || {}, animationOptions, this.autoAnimateCounter++ ); | ||||||
|  | 			} ); | ||||||
|  |  | ||||||
|  | 			// Animate unmatched elements, if enabled | ||||||
|  | 			if( toSlide.dataset.autoAnimateUnmatched !== 'false' && this.Reveal.getConfig().autoAnimateUnmatched === true ) { | ||||||
|  | 				this.getUnmatchedAutoAnimateElements( toSlide ).forEach( unmatchedElement => { | ||||||
|  | 					unmatchedElement.dataset.autoAnimateTarget = 'unmatched'; | ||||||
|  | 				} ); | ||||||
|  |  | ||||||
|  | 				css.push( `[data-auto-animate="running"] [data-auto-animate-target="unmatched"] { transition: opacity ${animationOptions.duration*0.8}s ease ${animationOptions.duration*0.2}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( 'autoanimate', { fromSlide: fromSlide, toSlide: toSlide, sheet: this.autoAnimateStyleSheet } ); | ||||||
|  |  | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	/** | ||||||
|  | 	 * Rolls back all changes that we've made to the DOM so | ||||||
|  | 	 * that as part of animating. | ||||||
|  | 	 */ | ||||||
|  | 	reset() { | ||||||
|  |  | ||||||
|  | 		// Reset slides | ||||||
|  | 		toArray( this.Reveal.getRevealElement().querySelectorAll( '[data-auto-animate]:not([data-auto-animate=""])' ) ).forEach( element => { | ||||||
|  | 			element.dataset.autoAnimate = ''; | ||||||
|  | 		} ); | ||||||
|  |  | ||||||
|  | 		// Reset elements | ||||||
|  | 		toArray( this.Reveal.getRevealElement().querySelectorAll( '[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; | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	/** | ||||||
|  | 	 * Auto-animates the properties of an element from their original | ||||||
|  | 	 * values to their new 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 | ||||||
|  | 	 */ | ||||||
|  | 	getAutoAnimateCSS( 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 ); | ||||||
|  |  | ||||||
|  | 		// 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.closest && element.parentNode ) { | ||||||
|  | 			let autoAnimatedParent = element.parentNode.closest( '[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 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 { | ||||||
|  | 				bounds = element.getBoundingClientRect(); | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			properties.x = bounds.x; | ||||||
|  | 			properties.y = bounds.y; | ||||||
|  | 			properties.width = bounds.width; | ||||||
|  | 			properties.height = bounds.height; | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		const computedStyles = getComputedStyle( element ); | ||||||
|  |  | ||||||
|  | 		// CSS styles | ||||||
|  | 		( elementOptions.styles || this.Reveal.getConfig().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 transiition | ||||||
|  | 			// each individual text property instead | ||||||
|  | 			if( pair.from.matches( textNodes ) ) { | ||||||
|  | 				pair.options = { scale: false }; | ||||||
|  | 			} | ||||||
|  | 			// Animate individual lines of code | ||||||
|  | 			else if( pair.from.matches( 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; | ||||||
|  |  | ||||||
|  | 		}, [] ); | ||||||
|  |  | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | } | ||||||
							
								
								
									
										304
									
								
								js/controllers/fragments.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										304
									
								
								js/controllers/fragments.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,304 @@ | |||||||
|  | import { extend, toArray } 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; | ||||||
|  |  | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	/** | ||||||
|  | 	 * Shows all fragments in the presentation. Used when | ||||||
|  | 	 * fragments are disabled presentation-wide. | ||||||
|  | 	 */ | ||||||
|  | 	showAll() { | ||||||
|  |  | ||||||
|  | 		toArray( this.Reveal.getSlidesElement().querySelectorAll( '.fragment' ) ).forEach( element => { | ||||||
|  | 			element.classList.add( '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' ); | ||||||
|  | 			let hiddenFragments = currentSlide.querySelectorAll( '.fragment: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 = toArray( 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 = toArray( horizontalSlide.querySelectorAll( '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 ); | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 				toArray( 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 ) { | ||||||
|  | 						if( !el.classList.contains( 'visible' ) ) changedFragments.shown.push( el ); | ||||||
|  | 						el.classList.add( 'visible' ); | ||||||
|  | 						el.classList.remove( 'current-fragment' ); | ||||||
|  |  | ||||||
|  | 						// Announce the fragments one by one to the Screen Reader | ||||||
|  | 						this.Reveal.announceStatus( this.Reveal.getStatusText( el ) ); | ||||||
|  |  | ||||||
|  | 						if( i === index ) { | ||||||
|  | 							el.classList.add( 'current-fragment' ); | ||||||
|  | 							this.Reveal.slideContent.startEmbeddedContent( el ); | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
|  | 					// Hidden fragments | ||||||
|  | 					else { | ||||||
|  | 						if( el.classList.contains( 'visible' ) ) changedFragments.hidden.push( el ); | ||||||
|  | 						el.classList.remove( 'visible' ); | ||||||
|  | 						el.classList.remove( 'current-fragment' ); | ||||||
|  | 					} | ||||||
|  |  | ||||||
|  | 				} ); | ||||||
|  |  | ||||||
|  | 				// 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; | ||||||
|  |  | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	/** | ||||||
|  | 	 * 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' ) ); | ||||||
|  | 			if( fragments.length ) { | ||||||
|  |  | ||||||
|  | 				// If no index is specified, find the current | ||||||
|  | 				if( typeof index !== 'number' ) { | ||||||
|  | 					let lastVisibleFragment = this.sort( currentSlide.querySelectorAll( '.fragment.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( 'fragmenthidden', { fragment: changedFragments.hidden[0], fragments: changedFragments.hidden } ); | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 				if( changedFragments.shown.length ) { | ||||||
|  | 					this.Reveal.dispatchEvent( 'fragmentshown', { fragment: changedFragments.shown[0], fragments: changedFragments.shown } ); | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 				this.Reveal.updateControls(); | ||||||
|  | 				this.Reveal.updateProgress(); | ||||||
|  |  | ||||||
|  | 				if( this.Reveal.getConfig().fragmentInURL ) { | ||||||
|  | 					this.Reveal.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 ); | ||||||
|  |  | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | } | ||||||
							
								
								
									
										249
									
								
								js/controllers/overview.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										249
									
								
								js/controllers/overview.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,249 @@ | |||||||
|  | import { SLIDES_SELECTOR } from '../utils/constants.js' | ||||||
|  | import { extend, toArray, 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 | ||||||
|  | 			toArray( this.Reveal.getRevealElement().querySelectorAll( 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( 'overviewshown', { | ||||||
|  | 				'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' ) ) { | ||||||
|  |  | ||||||
|  | 				toArray( hslide.querySelectorAll( '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 | ||||||
|  | 		toArray( this.Reveal.getBackgroundsElement().childNodes ).forEach( ( hbackground, h ) => { | ||||||
|  | 			transformElement( hbackground, 'translate3d(' + ( h * this.overviewSlideWidth ) + 'px, 0, 0)' ); | ||||||
|  |  | ||||||
|  | 			toArray( hbackground.querySelectorAll( '.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 | ||||||
|  | 			toArray( this.Reveal.getRevealElement().querySelectorAll( SLIDES_SELECTOR ) ).forEach( slide => { | ||||||
|  | 				transformElement( slide, '' ); | ||||||
|  |  | ||||||
|  | 				slide.removeEventListener( 'click', this.onSlideClicked, true ); | ||||||
|  | 			} ); | ||||||
|  |  | ||||||
|  | 			// Clean up changes made to backgrounds | ||||||
|  | 			toArray( this.Reveal.getBackgroundsElement().querySelectorAll( '.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( 'overviewhidden', { | ||||||
|  | 				'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 ); | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -7,8 +7,8 @@ export default class Plugins { | |||||||
|  |  | ||||||
| 	constructor() { | 	constructor() { | ||||||
|  |  | ||||||
| 		// Flags our current state (pending -> loading -> loaded) | 		// Flags our current state (idle -> loading -> loaded) | ||||||
| 		this.state = 'pending'; | 		this.state = 'idle'; | ||||||
|  |  | ||||||
| 		// An id:instance map of currently registed plugins | 		// An id:instance map of currently registed plugins | ||||||
| 		this.registeredPlugins = {}; | 		this.registeredPlugins = {}; | ||||||
|   | |||||||
							
								
								
									
										433
									
								
								js/controllers/slidecontent.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										433
									
								
								js/controllers/slidecontent.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,433 @@ | |||||||
|  | import { HORIZONTAL_SLIDES_SELECTOR, VERTICAL_SLIDES_SELECTOR } from '../utils/constants.js' | ||||||
|  | import { extend, toArray, closestParent } from '../utils/util.js' | ||||||
|  | import { isMobile } from '../utils/device.js' | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Handles loading, unloading and playback of slide | ||||||
|  |  * content such as images, videos and iframes. | ||||||
|  |  */ | ||||||
|  | export default class SlideContent { | ||||||
|  |  | ||||||
|  | 	constructor( Reveal ) { | ||||||
|  |  | ||||||
|  | 		this.Reveal = Reveal; | ||||||
|  |  | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	/** | ||||||
|  | 	 * 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 | ||||||
|  | 		toArray( slide.querySelectorAll( '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 | ||||||
|  | 		toArray( slide.querySelectorAll( 'video, audio' ) ).forEach( media => { | ||||||
|  | 			let sources = 0; | ||||||
|  |  | ||||||
|  | 			toArray( media.querySelectorAll( 'source[data-src]' ) ).forEach( source => { | ||||||
|  | 				source.setAttribute( 'src', source.getAttribute( 'data-src' ) ); | ||||||
|  | 				source.removeAttribute( 'data-src' ); | ||||||
|  | 				source.setAttribute( 'data-lazy-loaded', '' ); | ||||||
|  | 				sources += 1; | ||||||
|  | 			} ); | ||||||
|  |  | ||||||
|  | 			// 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 ) { | ||||||
|  | 					backgroundContent.style.backgroundImage = 'url('+ encodeURI( backgroundImage ) +')'; | ||||||
|  | 				} | ||||||
|  | 				// Videos | ||||||
|  | 				else if ( backgroundVideo && !this.Reveal.isSpeakerNotes() ) { | ||||||
|  | 					let video = document.createElement( 'video' ); | ||||||
|  |  | ||||||
|  | 					if( backgroundVideoLoop ) { | ||||||
|  | 						video.setAttribute( 'loop', '' ); | ||||||
|  | 					} | ||||||
|  |  | ||||||
|  | 					if( backgroundVideoMuted ) { | ||||||
|  | 						video.muted = true; | ||||||
|  | 					} | ||||||
|  |  | ||||||
|  | 					// Inline video playback works (at least in Mobile Safari) as | ||||||
|  | 					// long as the video is muted and the `playsinline` attribute is | ||||||
|  | 					// present | ||||||
|  | 					if( isMobile ) { | ||||||
|  | 						video.muted = true; | ||||||
|  | 						video.autoplay = true; | ||||||
|  | 						video.setAttribute( 'playsinline', '' ); | ||||||
|  | 					} | ||||||
|  |  | ||||||
|  | 					// Support comma separated lists of video sources | ||||||
|  | 					backgroundVideo.split( ',' ).forEach( source => { | ||||||
|  | 						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 ); | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	/** | ||||||
|  | 	 * 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 | ||||||
|  | 			toArray( background.querySelectorAll( 'iframe[src]' ) ).forEach( element => { | ||||||
|  | 				element.removeAttribute( 'src' ); | ||||||
|  | 			} ); | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Reset lazy-loaded media elements with src attributes | ||||||
|  | 		toArray( slide.querySelectorAll( '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 | ||||||
|  | 		toArray( slide.querySelectorAll( '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 ) => { | ||||||
|  | 			toArray( this.Reveal.getSlidesElement().querySelectorAll( '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 | ||||||
|  | 			toArray( element.querySelectorAll( '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 | ||||||
|  | 			toArray( element.querySelectorAll( 'video, audio' ) ).forEach( el => { | ||||||
|  | 				if( closestParent( el, '.fragment' ) && !closestParent( 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' ) || !!closestParent( 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 | ||||||
|  | 			toArray( element.querySelectorAll( 'iframe[src]' ) ).forEach( el => { | ||||||
|  | 				if( closestParent( el, '.fragment' ) && !closestParent( el, '.fragment.visible' ) ) { | ||||||
|  | 					return; | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 				this.startEmbeddedIframe( { target: el } ); | ||||||
|  | 			} ); | ||||||
|  |  | ||||||
|  | 			// Lazy loading iframes | ||||||
|  | 			toArray( element.querySelectorAll( 'iframe[data-src]' ) ).forEach( el => { | ||||||
|  | 				if( closestParent( el, '.fragment' ) && !closestParent( 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 = !!closestParent( event.target, 'html' ), | ||||||
|  | 			isVisible  		= !!closestParent( 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 = !!closestParent( event.target, 'html' ), | ||||||
|  | 				isVisible  		= !!closestParent( 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' ) || !!closestParent( 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 | ||||||
|  | 			toArray( element.querySelectorAll( '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 | ||||||
|  | 			toArray( element.querySelectorAll( 'iframe' ) ).forEach( el => { | ||||||
|  | 				if( el.contentWindow ) el.contentWindow.postMessage( 'slide:stop', '*' ); | ||||||
|  | 				el.removeEventListener( 'load', this.startEmbeddedIframe ); | ||||||
|  | 			}); | ||||||
|  |  | ||||||
|  | 			// YouTube postMessage API | ||||||
|  | 			toArray( element.querySelectorAll( '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 | ||||||
|  | 			toArray( element.querySelectorAll( '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 | ||||||
|  | 				toArray( element.querySelectorAll( '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' ); | ||||||
|  | 				} ); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | } | ||||||
							
								
								
									
										1865
									
								
								js/reveal.js
									
									
									
									
									
								
							
							
						
						
									
										1865
									
								
								js/reveal.js
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										15
									
								
								js/utils/device.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								js/utils/device.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | |||||||
|  | const UA = navigator.userAgent; | ||||||
|  | const testElement = document.createElement( 'div' ); | ||||||
|  |  | ||||||
|  | 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 ); | ||||||
|  |  | ||||||
|  | // Flags if we should use zoom instead of transform to scale | ||||||
|  | // up slides. Zoom produces crisper results but has a lot of | ||||||
|  | // xbrowser quirks so we only use it in whitelsited browsers. | ||||||
|  | export const supportsZoom = 'zoom' in testElement.style && !isMobile && | ||||||
|  | 				( isChrome || /Version\/[\d\.]+.*Safari/.test( UA ) ); | ||||||
| @@ -140,16 +140,22 @@ export const enterFullscreen = () => { | |||||||
|  * |  * | ||||||
|  * @param {string} value |  * @param {string} value | ||||||
|  */ |  */ | ||||||
| export const injectStyleSheet = ( value ) => { | export const createStyleSheet = ( value ) => { | ||||||
|  |  | ||||||
| 	let tag = document.createElement( 'style' ); | 	let tag = document.createElement( 'style' ); | ||||||
| 	tag.type = 'text/css'; | 	tag.type = 'text/css'; | ||||||
|  |  | ||||||
|  | 	if( value && value.length > 0 ) { | ||||||
| 		if( tag.styleSheet ) { | 		if( tag.styleSheet ) { | ||||||
| 			tag.styleSheet.cssText = value; | 			tag.styleSheet.cssText = value; | ||||||
| 		} | 		} | ||||||
| 		else { | 		else { | ||||||
| 			tag.appendChild( document.createTextNode( value ) ); | 			tag.appendChild( document.createTextNode( value ) ); | ||||||
| 		} | 		} | ||||||
| 	document.getElementsByTagName( 'head' )[0].appendChild( tag ); | 	} | ||||||
|  |  | ||||||
|  | 	document.head.appendChild( tag ); | ||||||
|  |  | ||||||
|  | 	return tag; | ||||||
|  |  | ||||||
| } | } | ||||||
| @@ -18,17 +18,6 @@ | |||||||
|  |  | ||||||
| 			<div class="slides"> | 			<div class="slides"> | ||||||
|  |  | ||||||
|                 <section data-markdown> |  | ||||||
|                     ```php |  | ||||||
|                     public function foo() |  | ||||||
|                     { |  | ||||||
|                         $foo = array( |  | ||||||
|                             'bar' => 'bar' |  | ||||||
|                         ) |  | ||||||
|                     } |  | ||||||
|                     ``` |  | ||||||
|                 </section> |  | ||||||
|  |  | ||||||
|                 <!-- Use external markdown resource, separate slides by three newlines; vertical slides by two newlines --> |                 <!-- Use external markdown resource, separate slides by three newlines; vertical slides by two newlines --> | ||||||
|                 <section data-markdown="example.md" data-separator="^\n\n\n" data-separator-vertical="^\n\n"></section> |                 <section data-markdown="example.md" data-separator="^\n\n\n" data-separator-vertical="^\n\n"></section> | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										614
									
								
								test/test.html
									
									
									
									
									
								
							
							
						
						
									
										614
									
								
								test/test.html
									
									
									
									
									
								
							| @@ -78,7 +78,619 @@ | |||||||
| 		</div> | 		</div> | ||||||
|  |  | ||||||
| 		<script src="../dist/reveal.min.js"></script> | 		<script src="../dist/reveal.min.js"></script> | ||||||
| 		<script src="test.js"></script> | 		<script> | ||||||
|  | 			// These tests expect the DOM to contain a presentation | ||||||
|  | 			// with the following slide structure: | ||||||
|  | 			// | ||||||
|  | 			// 1 | ||||||
|  | 			// 2 - Three sub-slides | ||||||
|  | 			// 3 - Three fragment elements | ||||||
|  | 			// 3 - Two fragments with same data-fragment-index | ||||||
|  | 			// 4 | ||||||
|  | 			Reveal.initialize().then( function() { | ||||||
|  |  | ||||||
|  | 				// --------------------------------------------------------------- | ||||||
|  | 				// DOM TESTS | ||||||
|  |  | ||||||
|  | 				QUnit.module( 'DOM' ); | ||||||
|  |  | ||||||
|  | 				QUnit.test( 'Initial slides classes', function( assert ) { | ||||||
|  | 					var horizontalSlides = document.querySelectorAll( '.reveal .slides>section' ) | ||||||
|  |  | ||||||
|  | 					assert.strictEqual( document.querySelectorAll( '.reveal .slides section.past' ).length, 0, 'no .past slides' ); | ||||||
|  | 					assert.strictEqual( document.querySelectorAll( '.reveal .slides section.present' ).length, 1, 'one .present slide' ); | ||||||
|  | 					assert.strictEqual( document.querySelectorAll( '.reveal .slides>section.future' ).length, horizontalSlides.length - 1, 'remaining horizontal slides are .future' ); | ||||||
|  |  | ||||||
|  | 					assert.strictEqual( document.querySelectorAll( '.reveal .slides section.stack' ).length, 2, 'two .stacks' ); | ||||||
|  |  | ||||||
|  | 					assert.ok( document.querySelectorAll( '.reveal .slides section.stack' )[0].querySelectorAll( '.future' ).length > 0, 'vertical slides are given .future' ); | ||||||
|  | 				}); | ||||||
|  |  | ||||||
|  | 				// --------------------------------------------------------------- | ||||||
|  | 				// API TESTS | ||||||
|  |  | ||||||
|  | 				QUnit.module( 'API' ); | ||||||
|  |  | ||||||
|  | 				QUnit.test( 'Reveal.isReady', function( assert ) { | ||||||
|  | 					assert.strictEqual( Reveal.isReady(), true, 'returns true' ); | ||||||
|  | 				}); | ||||||
|  |  | ||||||
|  | 				QUnit.test( 'Reveal.isOverview', function( assert ) { | ||||||
|  | 					assert.strictEqual( Reveal.isOverview(), false, 'false by default' ); | ||||||
|  |  | ||||||
|  | 					Reveal.toggleOverview(); | ||||||
|  | 					assert.strictEqual( Reveal.isOverview(), true, 'true after toggling on' ); | ||||||
|  |  | ||||||
|  | 					Reveal.toggleOverview(); | ||||||
|  | 					assert.strictEqual( Reveal.isOverview(), false, 'false after toggling off' ); | ||||||
|  | 				}); | ||||||
|  |  | ||||||
|  | 				QUnit.test( 'Reveal.isPaused', function( assert ) { | ||||||
|  | 					assert.strictEqual( Reveal.isPaused(), false, 'false by default' ); | ||||||
|  |  | ||||||
|  | 					Reveal.togglePause(); | ||||||
|  | 					assert.strictEqual( Reveal.isPaused(), true, 'true after pausing' ); | ||||||
|  |  | ||||||
|  | 					Reveal.togglePause(); | ||||||
|  | 					assert.strictEqual( Reveal.isPaused(), false, 'false after resuming' ); | ||||||
|  | 				}); | ||||||
|  |  | ||||||
|  | 				QUnit.test( 'Reveal.isFirstSlide', function( assert ) { | ||||||
|  | 					Reveal.slide( 0, 0 ); | ||||||
|  | 					assert.strictEqual( Reveal.isFirstSlide(), true, 'true after Reveal.slide( 0, 0 )' ); | ||||||
|  |  | ||||||
|  | 					Reveal.slide( 1, 0 ); | ||||||
|  | 					assert.strictEqual( Reveal.isFirstSlide(), false, 'false after Reveal.slide( 1, 0 )' ); | ||||||
|  |  | ||||||
|  | 					Reveal.slide( 0, 0 ); | ||||||
|  | 					assert.strictEqual( Reveal.isFirstSlide(), true, 'true after Reveal.slide( 0, 0 )' ); | ||||||
|  | 				}); | ||||||
|  |  | ||||||
|  | 				QUnit.test( 'Reveal.isFirstSlide after vertical slide', function( assert ) { | ||||||
|  | 					Reveal.slide( 1, 1 ); | ||||||
|  | 					Reveal.slide( 0, 0 ); | ||||||
|  | 					assert.strictEqual( Reveal.isFirstSlide(), true, 'true after Reveal.slide( 1, 1 ) and then Reveal.slide( 0, 0 )' ); | ||||||
|  | 				}); | ||||||
|  |  | ||||||
|  | 				QUnit.test( 'Reveal.isLastSlide', function( assert ) { | ||||||
|  | 					Reveal.slide( 0, 0 ); | ||||||
|  | 					assert.strictEqual( Reveal.isLastSlide(), false, 'false after Reveal.slide( 0, 0 )' ); | ||||||
|  |  | ||||||
|  | 					var lastSlideIndex = document.querySelectorAll( '.reveal .slides>section' ).length - 1; | ||||||
|  |  | ||||||
|  | 					Reveal.slide( lastSlideIndex, 0 ); | ||||||
|  | 					assert.strictEqual( Reveal.isLastSlide(), true, 'true after Reveal.slide( '+ lastSlideIndex +', 0 )' ); | ||||||
|  |  | ||||||
|  | 					Reveal.slide( 0, 0 ); | ||||||
|  | 					assert.strictEqual( Reveal.isLastSlide(), false, 'false after Reveal.slide( 0, 0 )' ); | ||||||
|  | 				}); | ||||||
|  |  | ||||||
|  | 				QUnit.test( 'Reveal.isLastSlide after vertical slide', function( assert ) { | ||||||
|  | 					var lastSlideIndex = document.querySelectorAll( '.reveal .slides>section' ).length - 1; | ||||||
|  |  | ||||||
|  | 					Reveal.slide( 1, 1 ); | ||||||
|  | 					Reveal.slide( lastSlideIndex ); | ||||||
|  | 					assert.strictEqual( Reveal.isLastSlide(), true, 'true after Reveal.slide( 1, 1 ) and then Reveal.slide( '+ lastSlideIndex +', 0 )' ); | ||||||
|  | 				}); | ||||||
|  |  | ||||||
|  | 				QUnit.test( 'Reveal.getTotalSlides', function( assert ) { | ||||||
|  | 					assert.strictEqual( Reveal.getTotalSlides(), 8, 'eight slides in total' ); | ||||||
|  | 				}); | ||||||
|  |  | ||||||
|  | 				QUnit.test( 'Reveal.getIndices', function( assert ) { | ||||||
|  | 					var indices = Reveal.getIndices(); | ||||||
|  |  | ||||||
|  | 					assert.ok( indices.hasOwnProperty( 'h' ), 'h exists' ); | ||||||
|  | 					assert.ok( indices.hasOwnProperty( 'v' ), 'v exists' ); | ||||||
|  | 					assert.ok( indices.hasOwnProperty( 'f' ), 'f exists' ); | ||||||
|  |  | ||||||
|  | 					Reveal.slide( 1, 0 ); | ||||||
|  | 					assert.strictEqual( Reveal.getIndices().h, 1, 'h 1' ); | ||||||
|  | 					assert.strictEqual( Reveal.getIndices().v, 0, 'v 0' ); | ||||||
|  |  | ||||||
|  | 					Reveal.slide( 1, 2 ); | ||||||
|  | 					assert.strictEqual( Reveal.getIndices().h, 1, 'h 1' ); | ||||||
|  | 					assert.strictEqual( Reveal.getIndices().v, 2, 'v 2' ); | ||||||
|  |  | ||||||
|  | 					Reveal.slide( 0, 0 ); | ||||||
|  | 					assert.strictEqual( Reveal.getIndices().h, 0, 'h 0' ); | ||||||
|  | 					assert.strictEqual( Reveal.getIndices().v, 0, 'v 0' ); | ||||||
|  | 				}); | ||||||
|  |  | ||||||
|  | 				QUnit.test( 'Reveal.getSlide', function( assert ) { | ||||||
|  | 					assert.equal( Reveal.getSlide( 0 ), document.querySelector( '.reveal .slides>section:first-child' ), 'gets correct first slide' ); | ||||||
|  | 					assert.equal( Reveal.getSlide( 1 ), document.querySelector( '.reveal .slides>section:nth-child(2)' ), 'no v index returns stack' ); | ||||||
|  | 					assert.equal( Reveal.getSlide( 1, 0 ), document.querySelector( '.reveal .slides>section:nth-child(2)>section:nth-child(1)' ), 'v index 0 returns first vertical child' ); | ||||||
|  | 					assert.equal( Reveal.getSlide( 1, 1 ), document.querySelector( '.reveal .slides>section:nth-child(2)>section:nth-child(2)' ), 'v index 1 returns second vertical child' ); | ||||||
|  |  | ||||||
|  | 					assert.strictEqual( Reveal.getSlide( 100 ), undefined, 'undefined when out of horizontal bounds' ); | ||||||
|  | 					assert.strictEqual( Reveal.getSlide( 1, 100 ), undefined, 'undefined when out of vertical bounds' ); | ||||||
|  | 				}); | ||||||
|  |  | ||||||
|  | 				QUnit.test( 'Reveal.getSlideBackground', function( assert ) { | ||||||
|  | 					assert.equal( Reveal.getSlideBackground( 0 ), document.querySelector( '.reveal .backgrounds>.slide-background:first-child' ), 'gets correct first background' ); | ||||||
|  | 					assert.equal( Reveal.getSlideBackground( 1 ), document.querySelector( '.reveal .backgrounds>.slide-background:nth-child(2)' ), 'no v index returns stack' ); | ||||||
|  | 					assert.equal( Reveal.getSlideBackground( 1, 0 ), document.querySelector( '.reveal .backgrounds>.slide-background:nth-child(2) .slide-background:nth-child(2)' ), 'v index 0 returns first vertical child' ); | ||||||
|  | 					assert.equal( Reveal.getSlideBackground( 1, 1 ), document.querySelector( '.reveal .backgrounds>.slide-background:nth-child(2) .slide-background:nth-child(3)' ), 'v index 1 returns second vertical child' ); | ||||||
|  |  | ||||||
|  | 					assert.strictEqual( Reveal.getSlideBackground( 100 ), undefined, 'undefined when out of horizontal bounds' ); | ||||||
|  | 					assert.strictEqual( Reveal.getSlideBackground( 1, 100 ), undefined, 'undefined when out of vertical bounds' ); | ||||||
|  | 				}); | ||||||
|  |  | ||||||
|  | 				QUnit.test( 'Reveal.getSlideNotes', function( assert ) { | ||||||
|  | 					Reveal.slide( 0, 0 ); | ||||||
|  | 					assert.ok( Reveal.getSlideNotes() === 'speaker notes 1', 'works with <aside class="notes">' ); | ||||||
|  |  | ||||||
|  | 					Reveal.slide( 1, 0 ); | ||||||
|  | 					assert.ok( Reveal.getSlideNotes() === 'speaker notes 2', 'works with <section data-notes="">' ); | ||||||
|  | 				}); | ||||||
|  |  | ||||||
|  | 				QUnit.test( 'Reveal.getPreviousSlide/getCurrentSlide', function( assert ) { | ||||||
|  | 					Reveal.slide( 0, 0 ); | ||||||
|  | 					Reveal.slide( 1, 0 ); | ||||||
|  |  | ||||||
|  | 					var firstSlide = document.querySelector( '.reveal .slides>section:first-child' ); | ||||||
|  | 					var secondSlide = document.querySelector( '.reveal .slides>section:nth-child(2)>section' ); | ||||||
|  |  | ||||||
|  | 					assert.equal( Reveal.getPreviousSlide(), firstSlide, 'previous is slide #0' ); | ||||||
|  | 					assert.equal( Reveal.getCurrentSlide(), secondSlide, 'current is slide #1' ); | ||||||
|  | 				}); | ||||||
|  |  | ||||||
|  | 				QUnit.test( 'Reveal.getProgress', function( assert ) { | ||||||
|  | 					Reveal.slide( 0, 0 ); | ||||||
|  | 					assert.strictEqual( Reveal.getProgress(), 0, 'progress is 0 on first slide' ); | ||||||
|  |  | ||||||
|  | 					var lastSlideIndex = document.querySelectorAll( '.reveal .slides>section' ).length - 1; | ||||||
|  |  | ||||||
|  | 					Reveal.slide( lastSlideIndex, 0 ); | ||||||
|  | 					assert.strictEqual( Reveal.getProgress(), 1, 'progress is 1 on last slide' ); | ||||||
|  | 				}); | ||||||
|  |  | ||||||
|  | 				QUnit.test( 'Reveal.getScale', function( assert ) { | ||||||
|  | 					assert.ok( typeof Reveal.getScale() === 'number', 'has scale' ); | ||||||
|  | 				}); | ||||||
|  |  | ||||||
|  | 				QUnit.test( 'Reveal.getConfig', function( assert ) { | ||||||
|  | 					assert.ok( typeof Reveal.getConfig() === 'object', 'has config' ); | ||||||
|  | 				}); | ||||||
|  |  | ||||||
|  | 				QUnit.test( 'Reveal.configure', function( assert ) { | ||||||
|  | 					assert.strictEqual( Reveal.getConfig().loop, false, '"loop" is false to start with' ); | ||||||
|  |  | ||||||
|  | 					Reveal.configure({ loop: true }); | ||||||
|  | 					assert.strictEqual( Reveal.getConfig().loop, true, '"loop" has changed to true' ); | ||||||
|  |  | ||||||
|  | 					Reveal.configure({ loop: false, customTestValue: 1 }); | ||||||
|  | 					assert.strictEqual( Reveal.getConfig().customTestValue, 1, 'supports custom values' ); | ||||||
|  | 				}); | ||||||
|  |  | ||||||
|  | 				QUnit.test( 'Reveal.availableRoutes', function( assert ) { | ||||||
|  | 					Reveal.slide( 0, 0 ); | ||||||
|  | 					assert.deepEqual( Reveal.availableRoutes(), { left: false, up: false, down: false, right: true }, 'correct for first slide' ); | ||||||
|  |  | ||||||
|  | 					Reveal.slide( 1, 0 ); | ||||||
|  | 					assert.deepEqual( Reveal.availableRoutes(), { left: true, up: false, down: true, right: true }, 'correct for vertical slide' ); | ||||||
|  | 				}); | ||||||
|  |  | ||||||
|  | 				QUnit.test( 'Reveal.next', function( assert ) { | ||||||
|  | 					Reveal.slide( 0, 0 ); | ||||||
|  |  | ||||||
|  | 					// Step through vertical child slides | ||||||
|  | 					Reveal.next(); | ||||||
|  | 					assert.deepEqual( Reveal.getIndices(), { h: 1, v: 0, f: undefined } ); | ||||||
|  |  | ||||||
|  | 					Reveal.next(); | ||||||
|  | 					assert.deepEqual( Reveal.getIndices(), { h: 1, v: 1, f: undefined } ); | ||||||
|  |  | ||||||
|  | 					Reveal.next(); | ||||||
|  | 					assert.deepEqual( Reveal.getIndices(), { h: 1, v: 2, f: undefined } ); | ||||||
|  |  | ||||||
|  | 					// Step through fragments | ||||||
|  | 					Reveal.next(); | ||||||
|  | 					assert.deepEqual( Reveal.getIndices(), { h: 2, v: 0, f: -1 } ); | ||||||
|  |  | ||||||
|  | 					Reveal.next(); | ||||||
|  | 					assert.deepEqual( Reveal.getIndices(), { h: 2, v: 0, f: 0 } ); | ||||||
|  |  | ||||||
|  | 					Reveal.next(); | ||||||
|  | 					assert.deepEqual( Reveal.getIndices(), { h: 2, v: 0, f: 1 } ); | ||||||
|  |  | ||||||
|  | 					Reveal.next(); | ||||||
|  | 					assert.deepEqual( Reveal.getIndices(), { h: 2, v: 0, f: 2 } ); | ||||||
|  | 				}); | ||||||
|  |  | ||||||
|  | 				QUnit.test( 'Reveal.next at end', function( assert ) { | ||||||
|  | 					Reveal.slide( 3 ); | ||||||
|  |  | ||||||
|  | 					// We're at the end, this should have no effect | ||||||
|  | 					Reveal.next(); | ||||||
|  | 					assert.deepEqual( Reveal.getIndices(), { h: 3, v: 0, f: undefined } ); | ||||||
|  |  | ||||||
|  | 					Reveal.next(); | ||||||
|  | 					assert.deepEqual( Reveal.getIndices(), { h: 3, v: 0, f: undefined } ); | ||||||
|  | 				}); | ||||||
|  |  | ||||||
|  |  | ||||||
|  | 				// --------------------------------------------------------------- | ||||||
|  | 				// FRAGMENT TESTS | ||||||
|  |  | ||||||
|  | 				QUnit.module( 'Fragments' ); | ||||||
|  |  | ||||||
|  | 				QUnit.test( 'Sliding to fragments', function( assert ) { | ||||||
|  | 					Reveal.slide( 2, 0, -1 ); | ||||||
|  | 					assert.deepEqual( Reveal.getIndices(), { h: 2, v: 0, f: -1 }, 'Reveal.slide( 2, 0, -1 )' ); | ||||||
|  |  | ||||||
|  | 					Reveal.slide( 2, 0, 0 ); | ||||||
|  | 					assert.deepEqual( Reveal.getIndices(), { h: 2, v: 0, f: 0 }, 'Reveal.slide( 2, 0, 0 )' ); | ||||||
|  |  | ||||||
|  | 					Reveal.slide( 2, 0, 2 ); | ||||||
|  | 					assert.deepEqual( Reveal.getIndices(), { h: 2, v: 0, f: 2 }, 'Reveal.slide( 2, 0, 2 )' ); | ||||||
|  |  | ||||||
|  | 					Reveal.slide( 2, 0, 1 ); | ||||||
|  | 					assert.deepEqual( Reveal.getIndices(), { h: 2, v: 0, f: 1 }, 'Reveal.slide( 2, 0, 1 )' ); | ||||||
|  | 				}); | ||||||
|  |  | ||||||
|  | 				QUnit.test( 'data-fragment is set on slide <section>', function( assert ) { | ||||||
|  | 					Reveal.slide( 2, 0, -1 ); | ||||||
|  | 					assert.deepEqual( Reveal.getCurrentSlide().getAttribute( 'data-fragment' ), '-1' ); | ||||||
|  |  | ||||||
|  | 					Reveal.slide( 2, 0, 2 ); | ||||||
|  | 					assert.deepEqual( Reveal.getCurrentSlide().getAttribute( 'data-fragment' ), '2' ); | ||||||
|  |  | ||||||
|  | 					Reveal.slide( 2, 0, 0 ); | ||||||
|  | 					assert.deepEqual( Reveal.getCurrentSlide().getAttribute( 'data-fragment' ), '0' ); | ||||||
|  |  | ||||||
|  | 					var fragmentSlide = Reveal.getCurrentSlide(); | ||||||
|  |  | ||||||
|  | 					Reveal.slide( 3, 0 ); | ||||||
|  | 					assert.deepEqual( fragmentSlide.getAttribute( 'data-fragment' ), '0', 'data-fragment persists when jumping to another slide' ); | ||||||
|  | 				}); | ||||||
|  |  | ||||||
|  | 				QUnit.test( 'Hiding all fragments', function( assert ) { | ||||||
|  | 					var fragmentSlide = document.querySelector( '#fragment-slides>section:nth-child(1)' ); | ||||||
|  |  | ||||||
|  | 					Reveal.slide( 2, 0, 0 ); | ||||||
|  | 					assert.strictEqual( fragmentSlide.querySelectorAll( '.fragment.visible' ).length, 1, 'one fragment visible when index is 0' ); | ||||||
|  |  | ||||||
|  | 					Reveal.slide( 2, 0, -1 ); | ||||||
|  | 					assert.strictEqual( fragmentSlide.querySelectorAll( '.fragment.visible' ).length, 0, 'no fragments visible when index is -1' ); | ||||||
|  | 				}); | ||||||
|  |  | ||||||
|  | 				QUnit.test( 'Current fragment', function( assert ) { | ||||||
|  | 					var fragmentSlide = document.querySelector( '#fragment-slides>section:nth-child(1)' ); | ||||||
|  | 					var lastFragmentIndex = [].slice.call( fragmentSlide.querySelectorAll( '.fragment' ) ).pop().getAttribute( 'data-fragment-index' ); | ||||||
|  |  | ||||||
|  | 					Reveal.slide( 2, 0 ); | ||||||
|  | 					assert.strictEqual( fragmentSlide.querySelectorAll( '.fragment.current-fragment' ).length, 0, 'no current fragment at index -1' ); | ||||||
|  |  | ||||||
|  | 					Reveal.slide( 2, 0, 0 ); | ||||||
|  | 					assert.strictEqual( fragmentSlide.querySelectorAll( '.fragment.current-fragment' ).length, 1, 'one current fragment at index 0' ); | ||||||
|  |  | ||||||
|  | 					Reveal.slide( 1, 0, 0 ); | ||||||
|  | 					assert.strictEqual( fragmentSlide.querySelectorAll( '.fragment.current-fragment' ).length, 0, 'no current fragment when navigating to previous slide' ); | ||||||
|  |  | ||||||
|  | 					Reveal.slide( 3, 0, 0 ); | ||||||
|  | 					assert.strictEqual( fragmentSlide.querySelectorAll( '.fragment.current-fragment' ).length, 0, 'no current fragment when navigating to next slide' ); | ||||||
|  |  | ||||||
|  | 					Reveal.slide( 2, 1, -1 ); | ||||||
|  | 					Reveal.prev(); | ||||||
|  | 					assert.strictEqual( fragmentSlide.querySelector( '.fragment.current-fragment' ).getAttribute( 'data-fragment-index' ), lastFragmentIndex, 'last fragment is current fragment when returning from future slide' ); | ||||||
|  | 				}); | ||||||
|  |  | ||||||
|  | 				QUnit.test( 'Stepping through fragments', function( assert ) { | ||||||
|  | 					Reveal.slide( 2, 0, -1 ); | ||||||
|  |  | ||||||
|  | 					// forwards: | ||||||
|  |  | ||||||
|  | 					Reveal.next(); | ||||||
|  | 					assert.deepEqual( Reveal.getIndices(), { h: 2, v: 0, f: 0 }, 'next() goes to next fragment' ); | ||||||
|  |  | ||||||
|  | 					Reveal.right(); | ||||||
|  | 					assert.deepEqual( Reveal.getIndices(), { h: 2, v: 0, f: 1 }, 'right() goes to next fragment' ); | ||||||
|  |  | ||||||
|  | 					Reveal.down(); | ||||||
|  | 					assert.deepEqual( Reveal.getIndices(), { h: 2, v: 0, f: 2 }, 'down() goes to next fragment' ); | ||||||
|  |  | ||||||
|  | 					Reveal.down(); // moves to f #3 | ||||||
|  |  | ||||||
|  | 					// backwards: | ||||||
|  |  | ||||||
|  | 					Reveal.prev(); | ||||||
|  | 					assert.deepEqual( Reveal.getIndices(), { h: 2, v: 0, f: 2 }, 'prev() goes to prev fragment' ); | ||||||
|  |  | ||||||
|  | 					Reveal.left(); | ||||||
|  | 					assert.deepEqual( Reveal.getIndices(), { h: 2, v: 0, f: 1 }, 'left() goes to prev fragment' ); | ||||||
|  |  | ||||||
|  | 					Reveal.up(); | ||||||
|  | 					assert.deepEqual( Reveal.getIndices(), { h: 2, v: 0, f: 0 }, 'up() goes to prev fragment' ); | ||||||
|  | 				}); | ||||||
|  |  | ||||||
|  | 				QUnit.test( 'Stepping past fragments', function( assert ) { | ||||||
|  | 					var fragmentSlide = document.querySelector( '#fragment-slides>section:nth-child(1)' ); | ||||||
|  |  | ||||||
|  | 					Reveal.slide( 0, 0, 0 ); | ||||||
|  | 					assert.equal( fragmentSlide.querySelectorAll( '.fragment.visible' ).length, 0, 'no fragments visible when on previous slide' ); | ||||||
|  |  | ||||||
|  | 					Reveal.slide( 3, 0, 0 ); | ||||||
|  | 					assert.equal( fragmentSlide.querySelectorAll( '.fragment.visible' ).length, 3, 'all fragments visible when on future slide' ); | ||||||
|  | 				}); | ||||||
|  |  | ||||||
|  | 				QUnit.test( 'Fragment indices', function( assert ) { | ||||||
|  | 					var fragmentSlide = document.querySelector( '#fragment-slides>section:nth-child(2)' ); | ||||||
|  |  | ||||||
|  | 					Reveal.slide( 3, 0, 0 ); | ||||||
|  | 					assert.equal( fragmentSlide.querySelectorAll( '.fragment.visible' ).length, 2, 'both fragments of same index are shown' ); | ||||||
|  |  | ||||||
|  | 					// This slide has three fragments, first one is index 0, second and third have index 1 | ||||||
|  | 					Reveal.slide( 2, 2, 0 ); | ||||||
|  | 					assert.equal( Reveal.getIndices().f, 0, 'returns correct index for first fragment' ); | ||||||
|  |  | ||||||
|  | 					Reveal.slide( 2, 2, 1 ); | ||||||
|  | 					assert.equal( Reveal.getIndices().f, 1, 'returns correct index for two fragments with same index' ); | ||||||
|  | 				}); | ||||||
|  |  | ||||||
|  | 				QUnit.test( 'Index generation', function( assert ) { | ||||||
|  | 					var fragmentSlide = document.querySelector( '#fragment-slides>section:nth-child(1)' ); | ||||||
|  |  | ||||||
|  | 					// These have no indices defined to start with | ||||||
|  | 					assert.equal( fragmentSlide.querySelectorAll( '.fragment' )[0].getAttribute( 'data-fragment-index' ), '0' ); | ||||||
|  | 					assert.equal( fragmentSlide.querySelectorAll( '.fragment' )[1].getAttribute( 'data-fragment-index' ), '1' ); | ||||||
|  | 					assert.equal( fragmentSlide.querySelectorAll( '.fragment' )[2].getAttribute( 'data-fragment-index' ), '2' ); | ||||||
|  | 				}); | ||||||
|  |  | ||||||
|  | 				QUnit.test( 'Index normalization', function( assert ) { | ||||||
|  | 					var fragmentSlide = document.querySelector( '#fragment-slides>section:nth-child(3)' ); | ||||||
|  |  | ||||||
|  | 					// These start out as 1-4-4 and should normalize to 0-1-1 | ||||||
|  | 					assert.equal( fragmentSlide.querySelectorAll( '.fragment' )[0].getAttribute( 'data-fragment-index' ), '0' ); | ||||||
|  | 					assert.equal( fragmentSlide.querySelectorAll( '.fragment' )[1].getAttribute( 'data-fragment-index' ), '1' ); | ||||||
|  | 					assert.equal( fragmentSlide.querySelectorAll( '.fragment' )[2].getAttribute( 'data-fragment-index' ), '1' ); | ||||||
|  | 				}); | ||||||
|  |  | ||||||
|  | 				QUnit.test( 'fragmentshown event', function( assert ) { | ||||||
|  | 					assert.expect( 2 ); | ||||||
|  | 					var done = assert.async( 2 ); | ||||||
|  |  | ||||||
|  | 					var _onEvent = function( event ) { | ||||||
|  | 						assert.ok( true, 'event fired' ); | ||||||
|  | 						done(); | ||||||
|  | 					} | ||||||
|  |  | ||||||
|  | 					Reveal.addEventListener( 'fragmentshown', _onEvent ); | ||||||
|  |  | ||||||
|  | 					Reveal.slide( 2, 0 ); | ||||||
|  | 					Reveal.slide( 2, 0 ); // should do nothing | ||||||
|  | 					Reveal.slide( 2, 0, 0 ); // should do nothing | ||||||
|  | 					Reveal.next(); | ||||||
|  | 					Reveal.next(); | ||||||
|  | 					Reveal.prev(); // shouldn't fire fragmentshown | ||||||
|  |  | ||||||
|  | 					Reveal.removeEventListener( 'fragmentshown', _onEvent ); | ||||||
|  | 				}); | ||||||
|  |  | ||||||
|  | 				QUnit.test( 'fragmenthidden event', function( assert ) { | ||||||
|  | 					assert.expect( 2 ); | ||||||
|  | 					var done = assert.async( 2 ); | ||||||
|  |  | ||||||
|  | 					var _onEvent = function( event ) { | ||||||
|  | 						assert.ok( true, 'event fired' ); | ||||||
|  | 						done(); | ||||||
|  | 					} | ||||||
|  |  | ||||||
|  | 					Reveal.addEventListener( 'fragmenthidden', _onEvent ); | ||||||
|  |  | ||||||
|  | 					Reveal.slide( 2, 0, 2 ); | ||||||
|  | 					Reveal.slide( 2, 0, 2 ); // should do nothing | ||||||
|  | 					Reveal.prev(); | ||||||
|  | 					Reveal.prev(); | ||||||
|  | 					Reveal.next(); // shouldn't fire fragmenthidden | ||||||
|  |  | ||||||
|  | 					Reveal.removeEventListener( 'fragmenthidden', _onEvent ); | ||||||
|  | 				}); | ||||||
|  |  | ||||||
|  |  | ||||||
|  | 				// --------------------------------------------------------------- | ||||||
|  | 				// AUTO-SLIDE TESTS | ||||||
|  |  | ||||||
|  | 				QUnit.module( 'Auto Sliding' ); | ||||||
|  |  | ||||||
|  | 				QUnit.test( 'Reveal.isAutoSliding', function( assert ) { | ||||||
|  | 					assert.strictEqual( Reveal.isAutoSliding(), false, 'false by default' ); | ||||||
|  |  | ||||||
|  | 					Reveal.configure({ autoSlide: 10000 }); | ||||||
|  | 					assert.strictEqual( Reveal.isAutoSliding(), true, 'true after starting' ); | ||||||
|  |  | ||||||
|  | 					Reveal.configure({ autoSlide: 0 }); | ||||||
|  | 					assert.strictEqual( Reveal.isAutoSliding(), false, 'false after setting to 0' ); | ||||||
|  | 				}); | ||||||
|  |  | ||||||
|  | 				QUnit.test( 'Reveal.toggleAutoSlide', function( assert ) { | ||||||
|  | 					Reveal.configure({ autoSlide: 10000 }); | ||||||
|  |  | ||||||
|  | 					Reveal.toggleAutoSlide(); | ||||||
|  | 					assert.strictEqual( Reveal.isAutoSliding(), false, 'false after first toggle' ); | ||||||
|  | 					Reveal.toggleAutoSlide(); | ||||||
|  | 					assert.strictEqual( Reveal.isAutoSliding(), true, 'true after second toggle' ); | ||||||
|  |  | ||||||
|  | 					Reveal.configure({ autoSlide: 0 }); | ||||||
|  | 				}); | ||||||
|  |  | ||||||
|  | 				QUnit.test( 'autoslidepaused', function( assert ) { | ||||||
|  | 					assert.expect( 1 ); | ||||||
|  | 					var done = assert.async(); | ||||||
|  |  | ||||||
|  | 					var _onEvent = function( event ) { | ||||||
|  | 						assert.ok( true, 'event fired' ); | ||||||
|  | 						done(); | ||||||
|  | 					} | ||||||
|  |  | ||||||
|  | 					Reveal.addEventListener( 'autoslidepaused', _onEvent ); | ||||||
|  | 					Reveal.configure({ autoSlide: 10000 }); | ||||||
|  | 					Reveal.toggleAutoSlide(); | ||||||
|  |  | ||||||
|  | 					// cleanup | ||||||
|  | 					Reveal.configure({ autoSlide: 0 }); | ||||||
|  | 					Reveal.removeEventListener( 'autoslidepaused', _onEvent ); | ||||||
|  | 				}); | ||||||
|  |  | ||||||
|  | 				QUnit.test( 'autoslideresumed', function( assert ) { | ||||||
|  | 					assert.expect( 1 ); | ||||||
|  | 					var done = assert.async(); | ||||||
|  |  | ||||||
|  | 					var _onEvent = function( event ) { | ||||||
|  | 						assert.ok( true, 'event fired' ); | ||||||
|  | 						done(); | ||||||
|  | 					} | ||||||
|  |  | ||||||
|  | 					Reveal.addEventListener( 'autoslideresumed', _onEvent ); | ||||||
|  | 					Reveal.configure({ autoSlide: 10000 }); | ||||||
|  | 					Reveal.toggleAutoSlide(); | ||||||
|  | 					Reveal.toggleAutoSlide(); | ||||||
|  |  | ||||||
|  | 					// cleanup | ||||||
|  | 					Reveal.configure({ autoSlide: 0 }); | ||||||
|  | 					Reveal.removeEventListener( 'autoslideresumed', _onEvent ); | ||||||
|  | 				}); | ||||||
|  |  | ||||||
|  |  | ||||||
|  | 				// --------------------------------------------------------------- | ||||||
|  | 				// CONFIGURATION VALUES | ||||||
|  |  | ||||||
|  | 				QUnit.module( 'Configuration' ); | ||||||
|  |  | ||||||
|  | 				QUnit.test( 'Controls', function( assert ) { | ||||||
|  | 					var controlsElement = document.querySelector( '.reveal>.controls' ); | ||||||
|  |  | ||||||
|  | 					Reveal.configure({ controls: false }); | ||||||
|  | 					assert.equal( controlsElement.style.display, 'none', 'controls are hidden' ); | ||||||
|  |  | ||||||
|  | 					Reveal.configure({ controls: true }); | ||||||
|  | 					assert.equal( controlsElement.style.display, 'block', 'controls are visible' ); | ||||||
|  | 				}); | ||||||
|  |  | ||||||
|  | 				QUnit.test( 'Progress', function( assert ) { | ||||||
|  | 					var progressElement = document.querySelector( '.reveal>.progress' ); | ||||||
|  |  | ||||||
|  | 					Reveal.configure({ progress: false }); | ||||||
|  | 					assert.equal( progressElement.style.display, 'none', 'progress are hidden' ); | ||||||
|  |  | ||||||
|  | 					Reveal.configure({ progress: true }); | ||||||
|  | 					assert.equal( progressElement.style.display, 'block', 'progress are visible' ); | ||||||
|  | 				}); | ||||||
|  |  | ||||||
|  | 				QUnit.test( 'Loop', function( assert ) { | ||||||
|  | 					Reveal.configure({ loop: true }); | ||||||
|  |  | ||||||
|  | 					Reveal.slide( 0, 0 ); | ||||||
|  |  | ||||||
|  | 					Reveal.left(); | ||||||
|  | 					assert.notEqual( Reveal.getIndices().h, 0, 'looped from start to end' ); | ||||||
|  |  | ||||||
|  | 					Reveal.right(); | ||||||
|  | 					assert.equal( Reveal.getIndices().h, 0, 'looped from end to start' ); | ||||||
|  |  | ||||||
|  | 					Reveal.configure({ loop: false }); | ||||||
|  | 				}); | ||||||
|  |  | ||||||
|  |  | ||||||
|  | 				// --------------------------------------------------------------- | ||||||
|  | 				// LAZY-LOADING TESTS | ||||||
|  |  | ||||||
|  | 				QUnit.module( 'Lazy-Loading' ); | ||||||
|  |  | ||||||
|  | 				QUnit.test( 'img with data-src', function( assert ) { | ||||||
|  | 					assert.strictEqual( document.querySelectorAll( '.reveal section img[src]' ).length, 1, 'Image source has been set' ); | ||||||
|  | 				}); | ||||||
|  |  | ||||||
|  | 				QUnit.test( 'video with data-src', function( assert ) { | ||||||
|  | 					assert.strictEqual( document.querySelectorAll( '.reveal section video[src]' ).length, 1, 'Video source has been set' ); | ||||||
|  | 				}); | ||||||
|  |  | ||||||
|  | 				QUnit.test( 'audio with data-src', function( assert ) { | ||||||
|  | 					assert.strictEqual( document.querySelectorAll( '.reveal section audio[src]' ).length, 1, 'Audio source has been set' ); | ||||||
|  | 				}); | ||||||
|  |  | ||||||
|  | 				QUnit.test( 'iframe with data-src', function( assert ) { | ||||||
|  | 					Reveal.slide( 0, 0 ); | ||||||
|  | 					assert.strictEqual( document.querySelectorAll( '.reveal section iframe[src]' ).length, 0, 'Iframe source is not set' ); | ||||||
|  | 					Reveal.slide( 2, 1 ); | ||||||
|  | 					assert.strictEqual( document.querySelectorAll( '.reveal section iframe[src]' ).length, 1, 'Iframe source is set' ); | ||||||
|  | 					Reveal.slide( 2, 2 ); | ||||||
|  | 					assert.strictEqual( document.querySelectorAll( '.reveal section iframe[src]' ).length, 0, 'Iframe source is not set' ); | ||||||
|  | 				}); | ||||||
|  |  | ||||||
|  | 				QUnit.test( 'background images', function( assert ) { | ||||||
|  | 					var imageSource1 = Reveal.getSlide( 0 ).getAttribute( 'data-background-image' ); | ||||||
|  | 					var imageSource2 = Reveal.getSlide( 1, 0 ).getAttribute( 'data-background' ); | ||||||
|  |  | ||||||
|  | 					// check that the images are applied to the background elements | ||||||
|  | 					assert.ok( Reveal.getSlideBackground( 0 ).querySelector( '.slide-background-content' ).style.backgroundImage.indexOf( imageSource1 ) !== -1, 'data-background-image worked' ); | ||||||
|  | 					assert.ok( Reveal.getSlideBackground( 1, 0 ).querySelector( '.slide-background-content' ).style.backgroundImage.indexOf( imageSource2 ) !== -1, 'data-background worked' ); | ||||||
|  | 				}); | ||||||
|  |  | ||||||
|  |  | ||||||
|  | 				// --------------------------------------------------------------- | ||||||
|  | 				// EVENT TESTS | ||||||
|  |  | ||||||
|  | 				QUnit.module( 'Events' ); | ||||||
|  |  | ||||||
|  | 				QUnit.test( 'slidechanged', function( assert ) { | ||||||
|  | 					assert.expect( 3 ); | ||||||
|  | 					var done = assert.async( 3 ); | ||||||
|  |  | ||||||
|  | 					var _onEvent = function( event ) { | ||||||
|  | 						assert.ok( true, 'event fired' ); | ||||||
|  | 						done(); | ||||||
|  | 					} | ||||||
|  |  | ||||||
|  | 					Reveal.addEventListener( 'slidechanged', _onEvent ); | ||||||
|  |  | ||||||
|  | 					Reveal.slide( 1, 0 ); // should trigger | ||||||
|  | 					Reveal.slide( 1, 0 ); // should do nothing | ||||||
|  | 					Reveal.next(); // should trigger | ||||||
|  | 					Reveal.slide( 3, 0 ); // should trigger | ||||||
|  | 					Reveal.next(); // should do nothing | ||||||
|  |  | ||||||
|  | 					Reveal.removeEventListener( 'slidechanged', _onEvent ); | ||||||
|  |  | ||||||
|  | 				}); | ||||||
|  |  | ||||||
|  | 				QUnit.test( 'paused', function( assert ) { | ||||||
|  | 					assert.expect( 1 ); | ||||||
|  | 					var done = assert.async(); | ||||||
|  |  | ||||||
|  | 					var _onEvent = function( event ) { | ||||||
|  | 						assert.ok( true, 'event fired' ); | ||||||
|  | 						done(); | ||||||
|  | 					} | ||||||
|  |  | ||||||
|  | 					Reveal.addEventListener( 'paused', _onEvent ); | ||||||
|  |  | ||||||
|  | 					Reveal.togglePause(); | ||||||
|  | 					Reveal.togglePause(); | ||||||
|  |  | ||||||
|  | 					Reveal.removeEventListener( 'paused', _onEvent ); | ||||||
|  | 				}); | ||||||
|  |  | ||||||
|  | 				QUnit.test( 'resumed', function( assert ) { | ||||||
|  | 					assert.expect( 1 ); | ||||||
|  | 					var done = assert.async(); | ||||||
|  |  | ||||||
|  | 					var _onEvent = function( event ) { | ||||||
|  | 						assert.ok( true, 'event fired' ); | ||||||
|  | 						done(); | ||||||
|  | 					} | ||||||
|  |  | ||||||
|  | 					Reveal.addEventListener( 'resumed', _onEvent ); | ||||||
|  |  | ||||||
|  | 					Reveal.togglePause(); | ||||||
|  | 					Reveal.togglePause(); | ||||||
|  |  | ||||||
|  | 					Reveal.removeEventListener( 'resumed', _onEvent ); | ||||||
|  | 				}); | ||||||
|  |  | ||||||
|  | 			} ); | ||||||
|  | 		</script> | ||||||
|  |  | ||||||
| 	</body> | 	</body> | ||||||
| </html> | </html> | ||||||
|   | |||||||
							
								
								
									
										612
									
								
								test/test.js
									
									
									
									
									
								
							
							
						
						
									
										612
									
								
								test/test.js
									
									
									
									
									
								
							| @@ -1,612 +0,0 @@ | |||||||
| // These tests expect the DOM to contain a presentation |  | ||||||
| // with the following slide structure: |  | ||||||
| // |  | ||||||
| // 1 |  | ||||||
| // 2 - Three sub-slides |  | ||||||
| // 3 - Three fragment elements |  | ||||||
| // 3 - Two fragments with same data-fragment-index |  | ||||||
| // 4 |  | ||||||
|  |  | ||||||
| Reveal.initialize().then( function() { |  | ||||||
|  |  | ||||||
| 	// --------------------------------------------------------------- |  | ||||||
| 	// DOM TESTS |  | ||||||
|  |  | ||||||
| 	QUnit.module( 'DOM' ); |  | ||||||
|  |  | ||||||
| 	QUnit.test( 'Initial slides classes', function( assert ) { |  | ||||||
| 		var horizontalSlides = document.querySelectorAll( '.reveal .slides>section' ) |  | ||||||
|  |  | ||||||
| 		assert.strictEqual( document.querySelectorAll( '.reveal .slides section.past' ).length, 0, 'no .past slides' ); |  | ||||||
| 		assert.strictEqual( document.querySelectorAll( '.reveal .slides section.present' ).length, 1, 'one .present slide' ); |  | ||||||
| 		assert.strictEqual( document.querySelectorAll( '.reveal .slides>section.future' ).length, horizontalSlides.length - 1, 'remaining horizontal slides are .future' ); |  | ||||||
|  |  | ||||||
| 		assert.strictEqual( document.querySelectorAll( '.reveal .slides section.stack' ).length, 2, 'two .stacks' ); |  | ||||||
|  |  | ||||||
| 		assert.ok( document.querySelectorAll( '.reveal .slides section.stack' )[0].querySelectorAll( '.future' ).length > 0, 'vertical slides are given .future' ); |  | ||||||
| 	}); |  | ||||||
|  |  | ||||||
| 	// --------------------------------------------------------------- |  | ||||||
| 	// API TESTS |  | ||||||
|  |  | ||||||
| 	QUnit.module( 'API' ); |  | ||||||
|  |  | ||||||
| 	QUnit.test( 'Reveal.isReady', function( assert ) { |  | ||||||
| 		assert.strictEqual( Reveal.isReady(), true, 'returns true' ); |  | ||||||
| 	}); |  | ||||||
|  |  | ||||||
| 	QUnit.test( 'Reveal.isOverview', function( assert ) { |  | ||||||
| 		assert.strictEqual( Reveal.isOverview(), false, 'false by default' ); |  | ||||||
|  |  | ||||||
| 		Reveal.toggleOverview(); |  | ||||||
| 		assert.strictEqual( Reveal.isOverview(), true, 'true after toggling on' ); |  | ||||||
|  |  | ||||||
| 		Reveal.toggleOverview(); |  | ||||||
| 		assert.strictEqual( Reveal.isOverview(), false, 'false after toggling off' ); |  | ||||||
| 	}); |  | ||||||
|  |  | ||||||
| 	QUnit.test( 'Reveal.isPaused', function( assert ) { |  | ||||||
| 		assert.strictEqual( Reveal.isPaused(), false, 'false by default' ); |  | ||||||
|  |  | ||||||
| 		Reveal.togglePause(); |  | ||||||
| 		assert.strictEqual( Reveal.isPaused(), true, 'true after pausing' ); |  | ||||||
|  |  | ||||||
| 		Reveal.togglePause(); |  | ||||||
| 		assert.strictEqual( Reveal.isPaused(), false, 'false after resuming' ); |  | ||||||
| 	}); |  | ||||||
|  |  | ||||||
| 	QUnit.test( 'Reveal.isFirstSlide', function( assert ) { |  | ||||||
| 		Reveal.slide( 0, 0 ); |  | ||||||
| 		assert.strictEqual( Reveal.isFirstSlide(), true, 'true after Reveal.slide( 0, 0 )' ); |  | ||||||
|  |  | ||||||
| 		Reveal.slide( 1, 0 ); |  | ||||||
| 		assert.strictEqual( Reveal.isFirstSlide(), false, 'false after Reveal.slide( 1, 0 )' ); |  | ||||||
|  |  | ||||||
| 		Reveal.slide( 0, 0 ); |  | ||||||
| 		assert.strictEqual( Reveal.isFirstSlide(), true, 'true after Reveal.slide( 0, 0 )' ); |  | ||||||
| 	}); |  | ||||||
|  |  | ||||||
| 	QUnit.test( 'Reveal.isFirstSlide after vertical slide', function( assert ) { |  | ||||||
| 		Reveal.slide( 1, 1 ); |  | ||||||
| 		Reveal.slide( 0, 0 ); |  | ||||||
| 		assert.strictEqual( Reveal.isFirstSlide(), true, 'true after Reveal.slide( 1, 1 ) and then Reveal.slide( 0, 0 )' ); |  | ||||||
| 	}); |  | ||||||
|  |  | ||||||
| 	QUnit.test( 'Reveal.isLastSlide', function( assert ) { |  | ||||||
| 		Reveal.slide( 0, 0 ); |  | ||||||
| 		assert.strictEqual( Reveal.isLastSlide(), false, 'false after Reveal.slide( 0, 0 )' ); |  | ||||||
|  |  | ||||||
| 		var lastSlideIndex = document.querySelectorAll( '.reveal .slides>section' ).length - 1; |  | ||||||
|  |  | ||||||
| 		Reveal.slide( lastSlideIndex, 0 ); |  | ||||||
| 		assert.strictEqual( Reveal.isLastSlide(), true, 'true after Reveal.slide( '+ lastSlideIndex +', 0 )' ); |  | ||||||
|  |  | ||||||
| 		Reveal.slide( 0, 0 ); |  | ||||||
| 		assert.strictEqual( Reveal.isLastSlide(), false, 'false after Reveal.slide( 0, 0 )' ); |  | ||||||
| 	}); |  | ||||||
|  |  | ||||||
| 	QUnit.test( 'Reveal.isLastSlide after vertical slide', function( assert ) { |  | ||||||
| 		var lastSlideIndex = document.querySelectorAll( '.reveal .slides>section' ).length - 1; |  | ||||||
|  |  | ||||||
| 		Reveal.slide( 1, 1 ); |  | ||||||
| 		Reveal.slide( lastSlideIndex ); |  | ||||||
| 		assert.strictEqual( Reveal.isLastSlide(), true, 'true after Reveal.slide( 1, 1 ) and then Reveal.slide( '+ lastSlideIndex +', 0 )' ); |  | ||||||
| 	}); |  | ||||||
|  |  | ||||||
| 	QUnit.test( 'Reveal.getTotalSlides', function( assert ) { |  | ||||||
| 		assert.strictEqual( Reveal.getTotalSlides(), 8, 'eight slides in total' ); |  | ||||||
| 	}); |  | ||||||
|  |  | ||||||
| 	QUnit.test( 'Reveal.getIndices', function( assert ) { |  | ||||||
| 		var indices = Reveal.getIndices(); |  | ||||||
|  |  | ||||||
| 		assert.ok( indices.hasOwnProperty( 'h' ), 'h exists' ); |  | ||||||
| 		assert.ok( indices.hasOwnProperty( 'v' ), 'v exists' ); |  | ||||||
| 		assert.ok( indices.hasOwnProperty( 'f' ), 'f exists' ); |  | ||||||
|  |  | ||||||
| 		Reveal.slide( 1, 0 ); |  | ||||||
| 		assert.strictEqual( Reveal.getIndices().h, 1, 'h 1' ); |  | ||||||
| 		assert.strictEqual( Reveal.getIndices().v, 0, 'v 0' ); |  | ||||||
|  |  | ||||||
| 		Reveal.slide( 1, 2 ); |  | ||||||
| 		assert.strictEqual( Reveal.getIndices().h, 1, 'h 1' ); |  | ||||||
| 		assert.strictEqual( Reveal.getIndices().v, 2, 'v 2' ); |  | ||||||
|  |  | ||||||
| 		Reveal.slide( 0, 0 ); |  | ||||||
| 		assert.strictEqual( Reveal.getIndices().h, 0, 'h 0' ); |  | ||||||
| 		assert.strictEqual( Reveal.getIndices().v, 0, 'v 0' ); |  | ||||||
| 	}); |  | ||||||
|  |  | ||||||
| 	QUnit.test( 'Reveal.getSlide', function( assert ) { |  | ||||||
| 		assert.equal( Reveal.getSlide( 0 ), document.querySelector( '.reveal .slides>section:first-child' ), 'gets correct first slide' ); |  | ||||||
| 		assert.equal( Reveal.getSlide( 1 ), document.querySelector( '.reveal .slides>section:nth-child(2)' ), 'no v index returns stack' ); |  | ||||||
| 		assert.equal( Reveal.getSlide( 1, 0 ), document.querySelector( '.reveal .slides>section:nth-child(2)>section:nth-child(1)' ), 'v index 0 returns first vertical child' ); |  | ||||||
| 		assert.equal( Reveal.getSlide( 1, 1 ), document.querySelector( '.reveal .slides>section:nth-child(2)>section:nth-child(2)' ), 'v index 1 returns second vertical child' ); |  | ||||||
|  |  | ||||||
| 		assert.strictEqual( Reveal.getSlide( 100 ), undefined, 'undefined when out of horizontal bounds' ); |  | ||||||
| 		assert.strictEqual( Reveal.getSlide( 1, 100 ), undefined, 'undefined when out of vertical bounds' ); |  | ||||||
| 	}); |  | ||||||
|  |  | ||||||
| 	QUnit.test( 'Reveal.getSlideBackground', function( assert ) { |  | ||||||
| 		assert.equal( Reveal.getSlideBackground( 0 ), document.querySelector( '.reveal .backgrounds>.slide-background:first-child' ), 'gets correct first background' ); |  | ||||||
| 		assert.equal( Reveal.getSlideBackground( 1 ), document.querySelector( '.reveal .backgrounds>.slide-background:nth-child(2)' ), 'no v index returns stack' ); |  | ||||||
| 		assert.equal( Reveal.getSlideBackground( 1, 0 ), document.querySelector( '.reveal .backgrounds>.slide-background:nth-child(2) .slide-background:nth-child(2)' ), 'v index 0 returns first vertical child' ); |  | ||||||
| 		assert.equal( Reveal.getSlideBackground( 1, 1 ), document.querySelector( '.reveal .backgrounds>.slide-background:nth-child(2) .slide-background:nth-child(3)' ), 'v index 1 returns second vertical child' ); |  | ||||||
|  |  | ||||||
| 		assert.strictEqual( Reveal.getSlideBackground( 100 ), undefined, 'undefined when out of horizontal bounds' ); |  | ||||||
| 		assert.strictEqual( Reveal.getSlideBackground( 1, 100 ), undefined, 'undefined when out of vertical bounds' ); |  | ||||||
| 	}); |  | ||||||
|  |  | ||||||
| 	QUnit.test( 'Reveal.getSlideNotes', function( assert ) { |  | ||||||
| 		Reveal.slide( 0, 0 ); |  | ||||||
| 		assert.ok( Reveal.getSlideNotes() === 'speaker notes 1', 'works with <aside class="notes">' ); |  | ||||||
|  |  | ||||||
| 		Reveal.slide( 1, 0 ); |  | ||||||
| 		assert.ok( Reveal.getSlideNotes() === 'speaker notes 2', 'works with <section data-notes="">' ); |  | ||||||
| 	}); |  | ||||||
|  |  | ||||||
| 	QUnit.test( 'Reveal.getPreviousSlide/getCurrentSlide', function( assert ) { |  | ||||||
| 		Reveal.slide( 0, 0 ); |  | ||||||
| 		Reveal.slide( 1, 0 ); |  | ||||||
|  |  | ||||||
| 		var firstSlide = document.querySelector( '.reveal .slides>section:first-child' ); |  | ||||||
| 		var secondSlide = document.querySelector( '.reveal .slides>section:nth-child(2)>section' ); |  | ||||||
|  |  | ||||||
| 		assert.equal( Reveal.getPreviousSlide(), firstSlide, 'previous is slide #0' ); |  | ||||||
| 		assert.equal( Reveal.getCurrentSlide(), secondSlide, 'current is slide #1' ); |  | ||||||
| 	}); |  | ||||||
|  |  | ||||||
| 	QUnit.test( 'Reveal.getProgress', function( assert ) { |  | ||||||
| 		Reveal.slide( 0, 0 ); |  | ||||||
| 		assert.strictEqual( Reveal.getProgress(), 0, 'progress is 0 on first slide' ); |  | ||||||
|  |  | ||||||
| 		var lastSlideIndex = document.querySelectorAll( '.reveal .slides>section' ).length - 1; |  | ||||||
|  |  | ||||||
| 		Reveal.slide( lastSlideIndex, 0 ); |  | ||||||
| 		assert.strictEqual( Reveal.getProgress(), 1, 'progress is 1 on last slide' ); |  | ||||||
| 	}); |  | ||||||
|  |  | ||||||
| 	QUnit.test( 'Reveal.getScale', function( assert ) { |  | ||||||
| 		assert.ok( typeof Reveal.getScale() === 'number', 'has scale' ); |  | ||||||
| 	}); |  | ||||||
|  |  | ||||||
| 	QUnit.test( 'Reveal.getConfig', function( assert ) { |  | ||||||
| 		assert.ok( typeof Reveal.getConfig() === 'object', 'has config' ); |  | ||||||
| 	}); |  | ||||||
|  |  | ||||||
| 	QUnit.test( 'Reveal.configure', function( assert ) { |  | ||||||
| 		assert.strictEqual( Reveal.getConfig().loop, false, '"loop" is false to start with' ); |  | ||||||
|  |  | ||||||
| 		Reveal.configure({ loop: true }); |  | ||||||
| 		assert.strictEqual( Reveal.getConfig().loop, true, '"loop" has changed to true' ); |  | ||||||
|  |  | ||||||
| 		Reveal.configure({ loop: false, customTestValue: 1 }); |  | ||||||
| 		assert.strictEqual( Reveal.getConfig().customTestValue, 1, 'supports custom values' ); |  | ||||||
| 	}); |  | ||||||
|  |  | ||||||
| 	QUnit.test( 'Reveal.availableRoutes', function( assert ) { |  | ||||||
| 		Reveal.slide( 0, 0 ); |  | ||||||
| 		assert.deepEqual( Reveal.availableRoutes(), { left: false, up: false, down: false, right: true }, 'correct for first slide' ); |  | ||||||
|  |  | ||||||
| 		Reveal.slide( 1, 0 ); |  | ||||||
| 		assert.deepEqual( Reveal.availableRoutes(), { left: true, up: false, down: true, right: true }, 'correct for vertical slide' ); |  | ||||||
| 	}); |  | ||||||
|  |  | ||||||
| 	QUnit.test( 'Reveal.next', function( assert ) { |  | ||||||
| 		Reveal.slide( 0, 0 ); |  | ||||||
|  |  | ||||||
| 		// Step through vertical child slides |  | ||||||
| 		Reveal.next(); |  | ||||||
| 		assert.deepEqual( Reveal.getIndices(), { h: 1, v: 0, f: undefined } ); |  | ||||||
|  |  | ||||||
| 		Reveal.next(); |  | ||||||
| 		assert.deepEqual( Reveal.getIndices(), { h: 1, v: 1, f: undefined } ); |  | ||||||
|  |  | ||||||
| 		Reveal.next(); |  | ||||||
| 		assert.deepEqual( Reveal.getIndices(), { h: 1, v: 2, f: undefined } ); |  | ||||||
|  |  | ||||||
| 		// Step through fragments |  | ||||||
| 		Reveal.next(); |  | ||||||
| 		assert.deepEqual( Reveal.getIndices(), { h: 2, v: 0, f: -1 } ); |  | ||||||
|  |  | ||||||
| 		Reveal.next(); |  | ||||||
| 		assert.deepEqual( Reveal.getIndices(), { h: 2, v: 0, f: 0 } ); |  | ||||||
|  |  | ||||||
| 		Reveal.next(); |  | ||||||
| 		assert.deepEqual( Reveal.getIndices(), { h: 2, v: 0, f: 1 } ); |  | ||||||
|  |  | ||||||
| 		Reveal.next(); |  | ||||||
| 		assert.deepEqual( Reveal.getIndices(), { h: 2, v: 0, f: 2 } ); |  | ||||||
| 	}); |  | ||||||
|  |  | ||||||
| 	QUnit.test( 'Reveal.next at end', function( assert ) { |  | ||||||
| 		Reveal.slide( 3 ); |  | ||||||
|  |  | ||||||
| 		// We're at the end, this should have no effect |  | ||||||
| 		Reveal.next(); |  | ||||||
| 		assert.deepEqual( Reveal.getIndices(), { h: 3, v: 0, f: undefined } ); |  | ||||||
|  |  | ||||||
| 		Reveal.next(); |  | ||||||
| 		assert.deepEqual( Reveal.getIndices(), { h: 3, v: 0, f: undefined } ); |  | ||||||
| 	}); |  | ||||||
|  |  | ||||||
|  |  | ||||||
| 	// --------------------------------------------------------------- |  | ||||||
| 	// FRAGMENT TESTS |  | ||||||
|  |  | ||||||
| 	QUnit.module( 'Fragments' ); |  | ||||||
|  |  | ||||||
| 	QUnit.test( 'Sliding to fragments', function( assert ) { |  | ||||||
| 		Reveal.slide( 2, 0, -1 ); |  | ||||||
| 		assert.deepEqual( Reveal.getIndices(), { h: 2, v: 0, f: -1 }, 'Reveal.slide( 2, 0, -1 )' ); |  | ||||||
|  |  | ||||||
| 		Reveal.slide( 2, 0, 0 ); |  | ||||||
| 		assert.deepEqual( Reveal.getIndices(), { h: 2, v: 0, f: 0 }, 'Reveal.slide( 2, 0, 0 )' ); |  | ||||||
|  |  | ||||||
| 		Reveal.slide( 2, 0, 2 ); |  | ||||||
| 		assert.deepEqual( Reveal.getIndices(), { h: 2, v: 0, f: 2 }, 'Reveal.slide( 2, 0, 2 )' ); |  | ||||||
|  |  | ||||||
| 		Reveal.slide( 2, 0, 1 ); |  | ||||||
| 		assert.deepEqual( Reveal.getIndices(), { h: 2, v: 0, f: 1 }, 'Reveal.slide( 2, 0, 1 )' ); |  | ||||||
| 	}); |  | ||||||
|  |  | ||||||
| 	QUnit.test( 'data-fragment is set on slide <section>', function( assert ) { |  | ||||||
| 		Reveal.slide( 2, 0, -1 ); |  | ||||||
| 		assert.deepEqual( Reveal.getCurrentSlide().getAttribute( 'data-fragment' ), '-1' ); |  | ||||||
|  |  | ||||||
| 		Reveal.slide( 2, 0, 2 ); |  | ||||||
| 		assert.deepEqual( Reveal.getCurrentSlide().getAttribute( 'data-fragment' ), '2' ); |  | ||||||
|  |  | ||||||
| 		Reveal.slide( 2, 0, 0 ); |  | ||||||
| 		assert.deepEqual( Reveal.getCurrentSlide().getAttribute( 'data-fragment' ), '0' ); |  | ||||||
|  |  | ||||||
| 		var fragmentSlide = Reveal.getCurrentSlide(); |  | ||||||
|  |  | ||||||
| 		Reveal.slide( 3, 0 ); |  | ||||||
| 		assert.deepEqual( fragmentSlide.getAttribute( 'data-fragment' ), '0', 'data-fragment persists when jumping to another slide' ); |  | ||||||
| 	}); |  | ||||||
|  |  | ||||||
| 	QUnit.test( 'Hiding all fragments', function( assert ) { |  | ||||||
| 		var fragmentSlide = document.querySelector( '#fragment-slides>section:nth-child(1)' ); |  | ||||||
|  |  | ||||||
| 		Reveal.slide( 2, 0, 0 ); |  | ||||||
| 		assert.strictEqual( fragmentSlide.querySelectorAll( '.fragment.visible' ).length, 1, 'one fragment visible when index is 0' ); |  | ||||||
|  |  | ||||||
| 		Reveal.slide( 2, 0, -1 ); |  | ||||||
| 		assert.strictEqual( fragmentSlide.querySelectorAll( '.fragment.visible' ).length, 0, 'no fragments visible when index is -1' ); |  | ||||||
| 	}); |  | ||||||
|  |  | ||||||
| 	QUnit.test( 'Current fragment', function( assert ) { |  | ||||||
| 		var fragmentSlide = document.querySelector( '#fragment-slides>section:nth-child(1)' ); |  | ||||||
| 		var lastFragmentIndex = [].slice.call( fragmentSlide.querySelectorAll( '.fragment' ) ).pop().getAttribute( 'data-fragment-index' ); |  | ||||||
|  |  | ||||||
| 		Reveal.slide( 2, 0 ); |  | ||||||
| 		assert.strictEqual( fragmentSlide.querySelectorAll( '.fragment.current-fragment' ).length, 0, 'no current fragment at index -1' ); |  | ||||||
|  |  | ||||||
| 		Reveal.slide( 2, 0, 0 ); |  | ||||||
| 		assert.strictEqual( fragmentSlide.querySelectorAll( '.fragment.current-fragment' ).length, 1, 'one current fragment at index 0' ); |  | ||||||
|  |  | ||||||
| 		Reveal.slide( 1, 0, 0 ); |  | ||||||
| 		assert.strictEqual( fragmentSlide.querySelectorAll( '.fragment.current-fragment' ).length, 0, 'no current fragment when navigating to previous slide' ); |  | ||||||
|  |  | ||||||
| 		Reveal.slide( 3, 0, 0 ); |  | ||||||
| 		assert.strictEqual( fragmentSlide.querySelectorAll( '.fragment.current-fragment' ).length, 0, 'no current fragment when navigating to next slide' ); |  | ||||||
|  |  | ||||||
| 		Reveal.slide( 2, 1, -1 ); |  | ||||||
| 		Reveal.prev(); |  | ||||||
| 		assert.strictEqual( fragmentSlide.querySelector( '.fragment.current-fragment' ).getAttribute( 'data-fragment-index' ), lastFragmentIndex, 'last fragment is current fragment when returning from future slide' ); |  | ||||||
| 	}); |  | ||||||
|  |  | ||||||
| 	QUnit.test( 'Stepping through fragments', function( assert ) { |  | ||||||
| 		Reveal.slide( 2, 0, -1 ); |  | ||||||
|  |  | ||||||
| 		// forwards: |  | ||||||
|  |  | ||||||
| 		Reveal.next(); |  | ||||||
| 		assert.deepEqual( Reveal.getIndices(), { h: 2, v: 0, f: 0 }, 'next() goes to next fragment' ); |  | ||||||
|  |  | ||||||
| 		Reveal.right(); |  | ||||||
| 		assert.deepEqual( Reveal.getIndices(), { h: 2, v: 0, f: 1 }, 'right() goes to next fragment' ); |  | ||||||
|  |  | ||||||
| 		Reveal.down(); |  | ||||||
| 		assert.deepEqual( Reveal.getIndices(), { h: 2, v: 0, f: 2 }, 'down() goes to next fragment' ); |  | ||||||
|  |  | ||||||
| 		Reveal.down(); // moves to f #3 |  | ||||||
|  |  | ||||||
| 		// backwards: |  | ||||||
|  |  | ||||||
| 		Reveal.prev(); |  | ||||||
| 		assert.deepEqual( Reveal.getIndices(), { h: 2, v: 0, f: 2 }, 'prev() goes to prev fragment' ); |  | ||||||
|  |  | ||||||
| 		Reveal.left(); |  | ||||||
| 		assert.deepEqual( Reveal.getIndices(), { h: 2, v: 0, f: 1 }, 'left() goes to prev fragment' ); |  | ||||||
|  |  | ||||||
| 		Reveal.up(); |  | ||||||
| 		assert.deepEqual( Reveal.getIndices(), { h: 2, v: 0, f: 0 }, 'up() goes to prev fragment' ); |  | ||||||
| 	}); |  | ||||||
|  |  | ||||||
| 	QUnit.test( 'Stepping past fragments', function( assert ) { |  | ||||||
| 		var fragmentSlide = document.querySelector( '#fragment-slides>section:nth-child(1)' ); |  | ||||||
|  |  | ||||||
| 		Reveal.slide( 0, 0, 0 ); |  | ||||||
| 		assert.equal( fragmentSlide.querySelectorAll( '.fragment.visible' ).length, 0, 'no fragments visible when on previous slide' ); |  | ||||||
|  |  | ||||||
| 		Reveal.slide( 3, 0, 0 ); |  | ||||||
| 		assert.equal( fragmentSlide.querySelectorAll( '.fragment.visible' ).length, 3, 'all fragments visible when on future slide' ); |  | ||||||
| 	}); |  | ||||||
|  |  | ||||||
| 	QUnit.test( 'Fragment indices', function( assert ) { |  | ||||||
| 		var fragmentSlide = document.querySelector( '#fragment-slides>section:nth-child(2)' ); |  | ||||||
|  |  | ||||||
| 		Reveal.slide( 3, 0, 0 ); |  | ||||||
| 		assert.equal( fragmentSlide.querySelectorAll( '.fragment.visible' ).length, 2, 'both fragments of same index are shown' ); |  | ||||||
|  |  | ||||||
| 		// This slide has three fragments, first one is index 0, second and third have index 1 |  | ||||||
| 		Reveal.slide( 2, 2, 0 ); |  | ||||||
| 		assert.equal( Reveal.getIndices().f, 0, 'returns correct index for first fragment' ); |  | ||||||
|  |  | ||||||
| 		Reveal.slide( 2, 2, 1 ); |  | ||||||
| 		assert.equal( Reveal.getIndices().f, 1, 'returns correct index for two fragments with same index' ); |  | ||||||
| 	}); |  | ||||||
|  |  | ||||||
| 	QUnit.test( 'Index generation', function( assert ) { |  | ||||||
| 		var fragmentSlide = document.querySelector( '#fragment-slides>section:nth-child(1)' ); |  | ||||||
|  |  | ||||||
| 		// These have no indices defined to start with |  | ||||||
| 		assert.equal( fragmentSlide.querySelectorAll( '.fragment' )[0].getAttribute( 'data-fragment-index' ), '0' ); |  | ||||||
| 		assert.equal( fragmentSlide.querySelectorAll( '.fragment' )[1].getAttribute( 'data-fragment-index' ), '1' ); |  | ||||||
| 		assert.equal( fragmentSlide.querySelectorAll( '.fragment' )[2].getAttribute( 'data-fragment-index' ), '2' ); |  | ||||||
| 	}); |  | ||||||
|  |  | ||||||
| 	QUnit.test( 'Index normalization', function( assert ) { |  | ||||||
| 		var fragmentSlide = document.querySelector( '#fragment-slides>section:nth-child(3)' ); |  | ||||||
|  |  | ||||||
| 		// These start out as 1-4-4 and should normalize to 0-1-1 |  | ||||||
| 		assert.equal( fragmentSlide.querySelectorAll( '.fragment' )[0].getAttribute( 'data-fragment-index' ), '0' ); |  | ||||||
| 		assert.equal( fragmentSlide.querySelectorAll( '.fragment' )[1].getAttribute( 'data-fragment-index' ), '1' ); |  | ||||||
| 		assert.equal( fragmentSlide.querySelectorAll( '.fragment' )[2].getAttribute( 'data-fragment-index' ), '1' ); |  | ||||||
| 	}); |  | ||||||
|  |  | ||||||
| 	QUnit.test( 'fragmentshown event', function( assert ) { |  | ||||||
| 		assert.expect( 2 ); |  | ||||||
| 		var done = assert.async( 2 ); |  | ||||||
|  |  | ||||||
| 		var _onEvent = function( event ) { |  | ||||||
| 			assert.ok( true, 'event fired' ); |  | ||||||
| 			done(); |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		Reveal.addEventListener( 'fragmentshown', _onEvent ); |  | ||||||
|  |  | ||||||
| 		Reveal.slide( 2, 0 ); |  | ||||||
| 		Reveal.slide( 2, 0 ); // should do nothing |  | ||||||
| 		Reveal.slide( 2, 0, 0 ); // should do nothing |  | ||||||
| 		Reveal.next(); |  | ||||||
| 		Reveal.next(); |  | ||||||
| 		Reveal.prev(); // shouldn't fire fragmentshown |  | ||||||
|  |  | ||||||
| 		Reveal.removeEventListener( 'fragmentshown', _onEvent ); |  | ||||||
| 	}); |  | ||||||
|  |  | ||||||
| 	QUnit.test( 'fragmenthidden event', function( assert ) { |  | ||||||
| 		assert.expect( 2 ); |  | ||||||
| 		var done = assert.async( 2 ); |  | ||||||
|  |  | ||||||
| 		var _onEvent = function( event ) { |  | ||||||
| 			assert.ok( true, 'event fired' ); |  | ||||||
| 			done(); |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		Reveal.addEventListener( 'fragmenthidden', _onEvent ); |  | ||||||
|  |  | ||||||
| 		Reveal.slide( 2, 0, 2 ); |  | ||||||
| 		Reveal.slide( 2, 0, 2 ); // should do nothing |  | ||||||
| 		Reveal.prev(); |  | ||||||
| 		Reveal.prev(); |  | ||||||
| 		Reveal.next(); // shouldn't fire fragmenthidden |  | ||||||
|  |  | ||||||
| 		Reveal.removeEventListener( 'fragmenthidden', _onEvent ); |  | ||||||
| 	}); |  | ||||||
|  |  | ||||||
|  |  | ||||||
| 	// --------------------------------------------------------------- |  | ||||||
| 	// AUTO-SLIDE TESTS |  | ||||||
|  |  | ||||||
| 	QUnit.module( 'Auto Sliding' ); |  | ||||||
|  |  | ||||||
| 	QUnit.test( 'Reveal.isAutoSliding', function( assert ) { |  | ||||||
| 		assert.strictEqual( Reveal.isAutoSliding(), false, 'false by default' ); |  | ||||||
|  |  | ||||||
| 		Reveal.configure({ autoSlide: 10000 }); |  | ||||||
| 		assert.strictEqual( Reveal.isAutoSliding(), true, 'true after starting' ); |  | ||||||
|  |  | ||||||
| 		Reveal.configure({ autoSlide: 0 }); |  | ||||||
| 		assert.strictEqual( Reveal.isAutoSliding(), false, 'false after setting to 0' ); |  | ||||||
| 	}); |  | ||||||
|  |  | ||||||
| 	QUnit.test( 'Reveal.toggleAutoSlide', function( assert ) { |  | ||||||
| 		Reveal.configure({ autoSlide: 10000 }); |  | ||||||
|  |  | ||||||
| 		Reveal.toggleAutoSlide(); |  | ||||||
| 		assert.strictEqual( Reveal.isAutoSliding(), false, 'false after first toggle' ); |  | ||||||
| 		Reveal.toggleAutoSlide(); |  | ||||||
| 		assert.strictEqual( Reveal.isAutoSliding(), true, 'true after second toggle' ); |  | ||||||
|  |  | ||||||
| 		Reveal.configure({ autoSlide: 0 }); |  | ||||||
| 	}); |  | ||||||
|  |  | ||||||
| 	QUnit.test( 'autoslidepaused', function( assert ) { |  | ||||||
| 		assert.expect( 1 ); |  | ||||||
| 		var done = assert.async(); |  | ||||||
|  |  | ||||||
| 		var _onEvent = function( event ) { |  | ||||||
| 			assert.ok( true, 'event fired' ); |  | ||||||
| 			done(); |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		Reveal.addEventListener( 'autoslidepaused', _onEvent ); |  | ||||||
| 		Reveal.configure({ autoSlide: 10000 }); |  | ||||||
| 		Reveal.toggleAutoSlide(); |  | ||||||
|  |  | ||||||
| 		// cleanup |  | ||||||
| 		Reveal.configure({ autoSlide: 0 }); |  | ||||||
| 		Reveal.removeEventListener( 'autoslidepaused', _onEvent ); |  | ||||||
| 	}); |  | ||||||
|  |  | ||||||
| 	QUnit.test( 'autoslideresumed', function( assert ) { |  | ||||||
| 		assert.expect( 1 ); |  | ||||||
| 		var done = assert.async(); |  | ||||||
|  |  | ||||||
| 		var _onEvent = function( event ) { |  | ||||||
| 			assert.ok( true, 'event fired' ); |  | ||||||
| 			done(); |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		Reveal.addEventListener( 'autoslideresumed', _onEvent ); |  | ||||||
| 		Reveal.configure({ autoSlide: 10000 }); |  | ||||||
| 		Reveal.toggleAutoSlide(); |  | ||||||
| 		Reveal.toggleAutoSlide(); |  | ||||||
|  |  | ||||||
| 		// cleanup |  | ||||||
| 		Reveal.configure({ autoSlide: 0 }); |  | ||||||
| 		Reveal.removeEventListener( 'autoslideresumed', _onEvent ); |  | ||||||
| 	}); |  | ||||||
|  |  | ||||||
|  |  | ||||||
| 	// --------------------------------------------------------------- |  | ||||||
| 	// CONFIGURATION VALUES |  | ||||||
|  |  | ||||||
| 	QUnit.module( 'Configuration' ); |  | ||||||
|  |  | ||||||
| 	QUnit.test( 'Controls', function( assert ) { |  | ||||||
| 		var controlsElement = document.querySelector( '.reveal>.controls' ); |  | ||||||
|  |  | ||||||
| 		Reveal.configure({ controls: false }); |  | ||||||
| 		assert.equal( controlsElement.style.display, 'none', 'controls are hidden' ); |  | ||||||
|  |  | ||||||
| 		Reveal.configure({ controls: true }); |  | ||||||
| 		assert.equal( controlsElement.style.display, 'block', 'controls are visible' ); |  | ||||||
| 	}); |  | ||||||
|  |  | ||||||
| 	QUnit.test( 'Progress', function( assert ) { |  | ||||||
| 		var progressElement = document.querySelector( '.reveal>.progress' ); |  | ||||||
|  |  | ||||||
| 		Reveal.configure({ progress: false }); |  | ||||||
| 		assert.equal( progressElement.style.display, 'none', 'progress are hidden' ); |  | ||||||
|  |  | ||||||
| 		Reveal.configure({ progress: true }); |  | ||||||
| 		assert.equal( progressElement.style.display, 'block', 'progress are visible' ); |  | ||||||
| 	}); |  | ||||||
|  |  | ||||||
| 	QUnit.test( 'Loop', function( assert ) { |  | ||||||
| 		Reveal.configure({ loop: true }); |  | ||||||
|  |  | ||||||
| 		Reveal.slide( 0, 0 ); |  | ||||||
|  |  | ||||||
| 		Reveal.left(); |  | ||||||
| 		assert.notEqual( Reveal.getIndices().h, 0, 'looped from start to end' ); |  | ||||||
|  |  | ||||||
| 		Reveal.right(); |  | ||||||
| 		assert.equal( Reveal.getIndices().h, 0, 'looped from end to start' ); |  | ||||||
|  |  | ||||||
| 		Reveal.configure({ loop: false }); |  | ||||||
| 	}); |  | ||||||
|  |  | ||||||
|  |  | ||||||
| 	// --------------------------------------------------------------- |  | ||||||
| 	// LAZY-LOADING TESTS |  | ||||||
|  |  | ||||||
| 	QUnit.module( 'Lazy-Loading' ); |  | ||||||
|  |  | ||||||
| 	QUnit.test( 'img with data-src', function( assert ) { |  | ||||||
| 		assert.strictEqual( document.querySelectorAll( '.reveal section img[src]' ).length, 1, 'Image source has been set' ); |  | ||||||
| 	}); |  | ||||||
|  |  | ||||||
| 	QUnit.test( 'video with data-src', function( assert ) { |  | ||||||
| 		assert.strictEqual( document.querySelectorAll( '.reveal section video[src]' ).length, 1, 'Video source has been set' ); |  | ||||||
| 	}); |  | ||||||
|  |  | ||||||
| 	QUnit.test( 'audio with data-src', function( assert ) { |  | ||||||
| 		assert.strictEqual( document.querySelectorAll( '.reveal section audio[src]' ).length, 1, 'Audio source has been set' ); |  | ||||||
| 	}); |  | ||||||
|  |  | ||||||
| 	QUnit.test( 'iframe with data-src', function( assert ) { |  | ||||||
| 		Reveal.slide( 0, 0 ); |  | ||||||
| 		assert.strictEqual( document.querySelectorAll( '.reveal section iframe[src]' ).length, 0, 'Iframe source is not set' ); |  | ||||||
| 		Reveal.slide( 2, 1 ); |  | ||||||
| 		assert.strictEqual( document.querySelectorAll( '.reveal section iframe[src]' ).length, 1, 'Iframe source is set' ); |  | ||||||
| 		Reveal.slide( 2, 2 ); |  | ||||||
| 		assert.strictEqual( document.querySelectorAll( '.reveal section iframe[src]' ).length, 0, 'Iframe source is not set' ); |  | ||||||
| 	}); |  | ||||||
|  |  | ||||||
| 	QUnit.test( 'background images', function( assert ) { |  | ||||||
| 		var imageSource1 = Reveal.getSlide( 0 ).getAttribute( 'data-background-image' ); |  | ||||||
| 		var imageSource2 = Reveal.getSlide( 1, 0 ).getAttribute( 'data-background' ); |  | ||||||
|  |  | ||||||
| 		// check that the images are applied to the background elements |  | ||||||
| 		assert.ok( Reveal.getSlideBackground( 0 ).querySelector( '.slide-background-content' ).style.backgroundImage.indexOf( imageSource1 ) !== -1, 'data-background-image worked' ); |  | ||||||
| 		assert.ok( Reveal.getSlideBackground( 1, 0 ).querySelector( '.slide-background-content' ).style.backgroundImage.indexOf( imageSource2 ) !== -1, 'data-background worked' ); |  | ||||||
| 	}); |  | ||||||
|  |  | ||||||
|  |  | ||||||
| 	// --------------------------------------------------------------- |  | ||||||
| 	// EVENT TESTS |  | ||||||
|  |  | ||||||
| 	QUnit.module( 'Events' ); |  | ||||||
|  |  | ||||||
| 	QUnit.test( 'slidechanged', function( assert ) { |  | ||||||
| 		assert.expect( 3 ); |  | ||||||
| 		var done = assert.async( 3 ); |  | ||||||
|  |  | ||||||
| 		var _onEvent = function( event ) { |  | ||||||
| 			assert.ok( true, 'event fired' ); |  | ||||||
| 			done(); |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		Reveal.addEventListener( 'slidechanged', _onEvent ); |  | ||||||
|  |  | ||||||
| 		Reveal.slide( 1, 0 ); // should trigger |  | ||||||
| 		Reveal.slide( 1, 0 ); // should do nothing |  | ||||||
| 		Reveal.next(); // should trigger |  | ||||||
| 		Reveal.slide( 3, 0 ); // should trigger |  | ||||||
| 		Reveal.next(); // should do nothing |  | ||||||
|  |  | ||||||
| 		Reveal.removeEventListener( 'slidechanged', _onEvent ); |  | ||||||
|  |  | ||||||
| 	}); |  | ||||||
|  |  | ||||||
| 	QUnit.test( 'paused', function( assert ) { |  | ||||||
| 		assert.expect( 1 ); |  | ||||||
| 		var done = assert.async(); |  | ||||||
|  |  | ||||||
| 		var _onEvent = function( event ) { |  | ||||||
| 			assert.ok( true, 'event fired' ); |  | ||||||
| 			done(); |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		Reveal.addEventListener( 'paused', _onEvent ); |  | ||||||
|  |  | ||||||
| 		Reveal.togglePause(); |  | ||||||
| 		Reveal.togglePause(); |  | ||||||
|  |  | ||||||
| 		Reveal.removeEventListener( 'paused', _onEvent ); |  | ||||||
| 	}); |  | ||||||
|  |  | ||||||
| 	QUnit.test( 'resumed', function( assert ) { |  | ||||||
| 		assert.expect( 1 ); |  | ||||||
| 		var done = assert.async(); |  | ||||||
|  |  | ||||||
| 		var _onEvent = function( event ) { |  | ||||||
| 			assert.ok( true, 'event fired' ); |  | ||||||
| 			done(); |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		Reveal.addEventListener( 'resumed', _onEvent ); |  | ||||||
|  |  | ||||||
| 		Reveal.togglePause(); |  | ||||||
| 		Reveal.togglePause(); |  | ||||||
|  |  | ||||||
| 		Reveal.removeEventListener( 'resumed', _onEvent ); |  | ||||||
| 	}); |  | ||||||
|  |  | ||||||
| } ); |  | ||||||
		Reference in New Issue
	
	Block a user