| Christophe Dervieux | e1893ae | 2021-10-07 17:09:02 +0200 | [diff] [blame] | 1 | import { queryAll, extend, createStyleSheet, matches, closest } from '../utils/util.js' | 
 | 2 | import { FRAGMENT_STYLE_REGEX } from '../utils/constants.js' | 
 | 3 |  | 
 | 4 | // Counter used to generate unique IDs for auto-animated elements | 
 | 5 | let autoAnimateCounter = 0; | 
 | 6 |  | 
 | 7 | /** | 
 | 8 |  * Automatically animates matching elements across | 
 | 9 |  * slides with the [data-auto-animate] attribute. | 
 | 10 |  */ | 
 | 11 | export default class AutoAnimate { | 
 | 12 |  | 
 | 13 | 	constructor( Reveal ) { | 
 | 14 |  | 
 | 15 | 		this.Reveal = Reveal; | 
 | 16 |  | 
 | 17 | 	} | 
 | 18 |  | 
 | 19 | 	/** | 
 | 20 | 	 * Runs an auto-animation between the given slides. | 
 | 21 | 	 * | 
 | 22 | 	 * @param  {HTMLElement} fromSlide | 
 | 23 | 	 * @param  {HTMLElement} toSlide | 
 | 24 | 	 */ | 
 | 25 | 	run( fromSlide, toSlide ) { | 
 | 26 |  | 
 | 27 | 		// Clean up after prior animations | 
 | 28 | 		this.reset(); | 
 | 29 |  | 
 | 30 | 		let allSlides = this.Reveal.getSlides(); | 
 | 31 | 		let toSlideIndex = allSlides.indexOf( toSlide ); | 
 | 32 | 		let fromSlideIndex = allSlides.indexOf( fromSlide ); | 
 | 33 |  | 
 | 34 | 		// Ensure that both slides are auto-animate targets with the same data-auto-animate-id value | 
 | 35 | 		// (including null if absent on both) and that data-auto-animate-restart isn't set on the | 
 | 36 | 		// physically latter slide (independent of slide direction) | 
 | 37 | 		if( fromSlide.hasAttribute( 'data-auto-animate' ) && toSlide.hasAttribute( 'data-auto-animate' ) | 
 | 38 | 				&& fromSlide.getAttribute( 'data-auto-animate-id' ) === toSlide.getAttribute( 'data-auto-animate-id' )  | 
 | 39 | 				&& !( toSlideIndex > fromSlideIndex ? toSlide : fromSlide ).hasAttribute( 'data-auto-animate-restart' ) ) { | 
 | 40 |  | 
 | 41 | 			// Create a new auto-animate sheet | 
 | 42 | 			this.autoAnimateStyleSheet = this.autoAnimateStyleSheet || createStyleSheet(); | 
 | 43 |  | 
 | 44 | 			let animationOptions = this.getAutoAnimateOptions( toSlide ); | 
 | 45 |  | 
 | 46 | 			// Set our starting state | 
 | 47 | 			fromSlide.dataset.autoAnimate = 'pending'; | 
 | 48 | 			toSlide.dataset.autoAnimate = 'pending'; | 
 | 49 |  | 
 | 50 | 			// Flag the navigation direction, needed for fragment buildup | 
 | 51 | 			animationOptions.slideDirection = toSlideIndex > fromSlideIndex ? 'forward' : 'backward'; | 
 | 52 |  | 
| Marc Kupietz | 09b7575 | 2023-10-07 09:32:19 +0200 | [diff] [blame] | 53 | 			// If the from-slide is hidden because it has moved outside | 
 | 54 | 			// the view distance, we need to temporarily show it while | 
 | 55 | 			// measuring | 
 | 56 | 			let fromSlideIsHidden = fromSlide.style.display === 'none'; | 
 | 57 | 			if( fromSlideIsHidden ) fromSlide.style.display = this.Reveal.getConfig().display; | 
 | 58 |  | 
| Christophe Dervieux | e1893ae | 2021-10-07 17:09:02 +0200 | [diff] [blame] | 59 | 			// Inject our auto-animate styles for this transition | 
 | 60 | 			let css = this.getAutoAnimatableElements( fromSlide, toSlide ).map( elements => { | 
 | 61 | 				return this.autoAnimateElements( elements.from, elements.to, elements.options || {}, animationOptions, autoAnimateCounter++ ); | 
 | 62 | 			} ); | 
 | 63 |  | 
| Marc Kupietz | 09b7575 | 2023-10-07 09:32:19 +0200 | [diff] [blame] | 64 | 			if( fromSlideIsHidden ) fromSlide.style.display = 'none'; | 
 | 65 |  | 
| Christophe Dervieux | e1893ae | 2021-10-07 17:09:02 +0200 | [diff] [blame] | 66 | 			// Animate unmatched elements, if enabled | 
 | 67 | 			if( toSlide.dataset.autoAnimateUnmatched !== 'false' && this.Reveal.getConfig().autoAnimateUnmatched === true ) { | 
 | 68 |  | 
 | 69 | 				// Our default timings for unmatched elements | 
 | 70 | 				let defaultUnmatchedDuration = animationOptions.duration * 0.8, | 
 | 71 | 					defaultUnmatchedDelay = animationOptions.duration * 0.2; | 
 | 72 |  | 
 | 73 | 				this.getUnmatchedAutoAnimateElements( toSlide ).forEach( unmatchedElement => { | 
 | 74 |  | 
 | 75 | 					let unmatchedOptions = this.getAutoAnimateOptions( unmatchedElement, animationOptions ); | 
 | 76 | 					let id = 'unmatched'; | 
 | 77 |  | 
 | 78 | 					// If there is a duration or delay set specifically for this | 
 | 79 | 					// element our unmatched elements should adhere to those | 
 | 80 | 					if( unmatchedOptions.duration !== animationOptions.duration || unmatchedOptions.delay !== animationOptions.delay ) { | 
 | 81 | 						id = 'unmatched-' + autoAnimateCounter++; | 
 | 82 | 						css.push( `[data-auto-animate="running"] [data-auto-animate-target="${id}"] { transition: opacity ${unmatchedOptions.duration}s ease ${unmatchedOptions.delay}s; }` ); | 
 | 83 | 					} | 
 | 84 |  | 
 | 85 | 					unmatchedElement.dataset.autoAnimateTarget = id; | 
 | 86 |  | 
 | 87 | 				}, this ); | 
 | 88 |  | 
 | 89 | 				// Our default transition for unmatched elements | 
 | 90 | 				css.push( `[data-auto-animate="running"] [data-auto-animate-target="unmatched"] { transition: opacity ${defaultUnmatchedDuration}s ease ${defaultUnmatchedDelay}s; }` ); | 
 | 91 |  | 
 | 92 | 			} | 
 | 93 |  | 
 | 94 | 			// Setting the whole chunk of CSS at once is the most | 
 | 95 | 			// efficient way to do this. Using sheet.insertRule | 
 | 96 | 			// is multiple factors slower. | 
 | 97 | 			this.autoAnimateStyleSheet.innerHTML = css.join( '' ); | 
 | 98 |  | 
 | 99 | 			// Start the animation next cycle | 
 | 100 | 			requestAnimationFrame( () => { | 
 | 101 | 				if( this.autoAnimateStyleSheet ) { | 
 | 102 | 					// This forces our newly injected styles to be applied in Firefox | 
 | 103 | 					getComputedStyle( this.autoAnimateStyleSheet ).fontWeight; | 
 | 104 |  | 
 | 105 | 					toSlide.dataset.autoAnimate = 'running'; | 
 | 106 | 				} | 
 | 107 | 			} ); | 
 | 108 |  | 
 | 109 | 			this.Reveal.dispatchEvent({ | 
 | 110 | 				type: 'autoanimate', | 
 | 111 | 				data: { | 
 | 112 | 					fromSlide, | 
 | 113 | 					toSlide, | 
 | 114 | 					sheet: this.autoAnimateStyleSheet | 
 | 115 | 				} | 
 | 116 | 			}); | 
 | 117 |  | 
 | 118 | 		} | 
 | 119 |  | 
 | 120 | 	} | 
 | 121 |  | 
 | 122 | 	/** | 
 | 123 | 	 * Rolls back all changes that we've made to the DOM so | 
 | 124 | 	 * that as part of animating. | 
 | 125 | 	 */ | 
 | 126 | 	reset() { | 
 | 127 |  | 
 | 128 | 		// Reset slides | 
 | 129 | 		queryAll( this.Reveal.getRevealElement(), '[data-auto-animate]:not([data-auto-animate=""])' ).forEach( element => { | 
 | 130 | 			element.dataset.autoAnimate = ''; | 
 | 131 | 		} ); | 
 | 132 |  | 
 | 133 | 		// Reset elements | 
 | 134 | 		queryAll( this.Reveal.getRevealElement(), '[data-auto-animate-target]' ).forEach( element => { | 
 | 135 | 			delete element.dataset.autoAnimateTarget; | 
 | 136 | 		} ); | 
 | 137 |  | 
 | 138 | 		// Remove the animation sheet | 
 | 139 | 		if( this.autoAnimateStyleSheet && this.autoAnimateStyleSheet.parentNode ) { | 
 | 140 | 			this.autoAnimateStyleSheet.parentNode.removeChild( this.autoAnimateStyleSheet ); | 
 | 141 | 			this.autoAnimateStyleSheet = null; | 
 | 142 | 		} | 
 | 143 |  | 
 | 144 | 	} | 
 | 145 |  | 
 | 146 | 	/** | 
 | 147 | 	 * Creates a FLIP animation where the `to` element starts out | 
 | 148 | 	 * in the `from` element position and animates to its original | 
 | 149 | 	 * state. | 
 | 150 | 	 * | 
 | 151 | 	 * @param {HTMLElement} from | 
 | 152 | 	 * @param {HTMLElement} to | 
 | 153 | 	 * @param {Object} elementOptions Options for this element pair | 
 | 154 | 	 * @param {Object} animationOptions Options set at the slide level | 
 | 155 | 	 * @param {String} id Unique ID that we can use to identify this | 
 | 156 | 	 * auto-animate element in the DOM | 
 | 157 | 	 */ | 
 | 158 | 	autoAnimateElements( from, to, elementOptions, animationOptions, id ) { | 
 | 159 |  | 
 | 160 | 		// 'from' elements are given a data-auto-animate-target with no value, | 
 | 161 | 		// 'to' elements are are given a data-auto-animate-target with an ID | 
 | 162 | 		from.dataset.autoAnimateTarget = ''; | 
 | 163 | 		to.dataset.autoAnimateTarget = id; | 
 | 164 |  | 
 | 165 | 		// Each element may override any of the auto-animate options | 
 | 166 | 		// like transition easing, duration and delay via data-attributes | 
 | 167 | 		let options = this.getAutoAnimateOptions( to, animationOptions ); | 
 | 168 |  | 
 | 169 | 		// If we're using a custom element matcher the element options | 
 | 170 | 		// may contain additional transition overrides | 
 | 171 | 		if( typeof elementOptions.delay !== 'undefined' ) options.delay = elementOptions.delay; | 
 | 172 | 		if( typeof elementOptions.duration !== 'undefined' ) options.duration = elementOptions.duration; | 
 | 173 | 		if( typeof elementOptions.easing !== 'undefined' ) options.easing = elementOptions.easing; | 
 | 174 |  | 
 | 175 | 		let fromProps = this.getAutoAnimatableProperties( 'from', from, elementOptions ), | 
 | 176 | 			toProps = this.getAutoAnimatableProperties( 'to', to, elementOptions ); | 
 | 177 |  | 
 | 178 | 		// Maintain fragment visibility for matching elements when | 
 | 179 | 		// we're navigating forwards, this way the viewer won't need | 
 | 180 | 		// to step through the same fragments twice | 
 | 181 | 		if( to.classList.contains( 'fragment' ) ) { | 
 | 182 |  | 
 | 183 | 			// Don't auto-animate the opacity of fragments to avoid | 
 | 184 | 			// conflicts with fragment animations | 
 | 185 | 			delete toProps.styles['opacity']; | 
 | 186 |  | 
 | 187 | 			if( from.classList.contains( 'fragment' ) ) { | 
 | 188 |  | 
 | 189 | 				let fromFragmentStyle = ( from.className.match( FRAGMENT_STYLE_REGEX ) || [''] )[0]; | 
 | 190 | 				let toFragmentStyle = ( to.className.match( FRAGMENT_STYLE_REGEX ) || [''] )[0]; | 
 | 191 |  | 
 | 192 | 				// Only skip the fragment if the fragment animation style | 
 | 193 | 				// remains unchanged | 
 | 194 | 				if( fromFragmentStyle === toFragmentStyle && animationOptions.slideDirection === 'forward' ) { | 
 | 195 | 					to.classList.add( 'visible', 'disabled' ); | 
 | 196 | 				} | 
 | 197 |  | 
 | 198 | 			} | 
 | 199 |  | 
 | 200 | 		} | 
 | 201 |  | 
 | 202 | 		// If translation and/or scaling are enabled, css transform | 
 | 203 | 		// the 'to' element so that it matches the position and size | 
 | 204 | 		// of the 'from' element | 
 | 205 | 		if( elementOptions.translate !== false || elementOptions.scale !== false ) { | 
 | 206 |  | 
 | 207 | 			let presentationScale = this.Reveal.getScale(); | 
 | 208 |  | 
 | 209 | 			let delta = { | 
 | 210 | 				x: ( fromProps.x - toProps.x ) / presentationScale, | 
 | 211 | 				y: ( fromProps.y - toProps.y ) / presentationScale, | 
 | 212 | 				scaleX: fromProps.width / toProps.width, | 
 | 213 | 				scaleY: fromProps.height / toProps.height | 
 | 214 | 			}; | 
 | 215 |  | 
 | 216 | 			// Limit decimal points to avoid 0.0001px blur and stutter | 
 | 217 | 			delta.x = Math.round( delta.x * 1000 ) / 1000; | 
 | 218 | 			delta.y = Math.round( delta.y * 1000 ) / 1000; | 
 | 219 | 			delta.scaleX = Math.round( delta.scaleX * 1000 ) / 1000; | 
 | 220 | 			delta.scaleX = Math.round( delta.scaleX * 1000 ) / 1000; | 
 | 221 |  | 
 | 222 | 			let translate = elementOptions.translate !== false && ( delta.x !== 0 || delta.y !== 0 ), | 
 | 223 | 				scale = elementOptions.scale !== false && ( delta.scaleX !== 0 || delta.scaleY !== 0 ); | 
 | 224 |  | 
 | 225 | 			// No need to transform if nothing's changed | 
 | 226 | 			if( translate || scale ) { | 
 | 227 |  | 
 | 228 | 				let transform = []; | 
 | 229 |  | 
 | 230 | 				if( translate ) transform.push( `translate(${delta.x}px, ${delta.y}px)` ); | 
 | 231 | 				if( scale ) transform.push( `scale(${delta.scaleX}, ${delta.scaleY})` ); | 
 | 232 |  | 
 | 233 | 				fromProps.styles['transform'] = transform.join( ' ' ); | 
 | 234 | 				fromProps.styles['transform-origin'] = 'top left'; | 
 | 235 |  | 
 | 236 | 				toProps.styles['transform'] = 'none'; | 
 | 237 |  | 
 | 238 | 			} | 
 | 239 |  | 
 | 240 | 		} | 
 | 241 |  | 
 | 242 | 		// Delete all unchanged 'to' styles | 
 | 243 | 		for( let propertyName in toProps.styles ) { | 
 | 244 | 			const toValue = toProps.styles[propertyName]; | 
 | 245 | 			const fromValue = fromProps.styles[propertyName]; | 
 | 246 |  | 
 | 247 | 			if( toValue === fromValue ) { | 
 | 248 | 				delete toProps.styles[propertyName]; | 
 | 249 | 			} | 
 | 250 | 			else { | 
 | 251 | 				// If these property values were set via a custom matcher providing | 
 | 252 | 				// an explicit 'from' and/or 'to' value, we always inject those values. | 
 | 253 | 				if( toValue.explicitValue === true ) { | 
 | 254 | 					toProps.styles[propertyName] = toValue.value; | 
 | 255 | 				} | 
 | 256 |  | 
 | 257 | 				if( fromValue.explicitValue === true ) { | 
 | 258 | 					fromProps.styles[propertyName] = fromValue.value; | 
 | 259 | 				} | 
 | 260 | 			} | 
 | 261 | 		} | 
 | 262 |  | 
 | 263 | 		let css = ''; | 
 | 264 |  | 
 | 265 | 		let toStyleProperties = Object.keys( toProps.styles ); | 
 | 266 |  | 
 | 267 | 		// Only create animate this element IF at least one style | 
 | 268 | 		// property has changed | 
 | 269 | 		if( toStyleProperties.length > 0 ) { | 
 | 270 |  | 
 | 271 | 			// Instantly move to the 'from' state | 
 | 272 | 			fromProps.styles['transition'] = 'none'; | 
 | 273 |  | 
 | 274 | 			// Animate towards the 'to' state | 
 | 275 | 			toProps.styles['transition'] = `all ${options.duration}s ${options.easing} ${options.delay}s`; | 
 | 276 | 			toProps.styles['transition-property'] = toStyleProperties.join( ', ' ); | 
 | 277 | 			toProps.styles['will-change'] = toStyleProperties.join( ', ' ); | 
 | 278 |  | 
 | 279 | 			// Build up our custom CSS. We need to override inline styles | 
 | 280 | 			// so we need to make our styles vErY IMPORTANT!1!! | 
 | 281 | 			let fromCSS = Object.keys( fromProps.styles ).map( propertyName => { | 
 | 282 | 				return propertyName + ': ' + fromProps.styles[propertyName] + ' !important;'; | 
 | 283 | 			} ).join( '' ); | 
 | 284 |  | 
 | 285 | 			let toCSS = Object.keys( toProps.styles ).map( propertyName => { | 
 | 286 | 				return propertyName + ': ' + toProps.styles[propertyName] + ' !important;'; | 
 | 287 | 			} ).join( '' ); | 
 | 288 |  | 
 | 289 | 			css = 	'[data-auto-animate-target="'+ id +'"] {'+ fromCSS +'}' + | 
 | 290 | 					'[data-auto-animate="running"] [data-auto-animate-target="'+ id +'"] {'+ toCSS +'}'; | 
 | 291 |  | 
 | 292 | 		} | 
 | 293 |  | 
 | 294 | 		return css; | 
 | 295 |  | 
 | 296 | 	} | 
 | 297 |  | 
 | 298 | 	/** | 
 | 299 | 	 * Returns the auto-animate options for the given element. | 
 | 300 | 	 * | 
 | 301 | 	 * @param {HTMLElement} element Element to pick up options | 
 | 302 | 	 * from, either a slide or an animation target | 
 | 303 | 	 * @param {Object} [inheritedOptions] Optional set of existing | 
 | 304 | 	 * options | 
 | 305 | 	 */ | 
 | 306 | 	getAutoAnimateOptions( element, inheritedOptions ) { | 
 | 307 |  | 
 | 308 | 		let options = { | 
 | 309 | 			easing: this.Reveal.getConfig().autoAnimateEasing, | 
 | 310 | 			duration: this.Reveal.getConfig().autoAnimateDuration, | 
 | 311 | 			delay: 0 | 
 | 312 | 		}; | 
 | 313 |  | 
 | 314 | 		options = extend( options, inheritedOptions ); | 
 | 315 |  | 
 | 316 | 		// Inherit options from parent elements | 
 | 317 | 		if( element.parentNode ) { | 
 | 318 | 			let autoAnimatedParent = closest( element.parentNode, '[data-auto-animate-target]' ); | 
 | 319 | 			if( autoAnimatedParent ) { | 
 | 320 | 				options = this.getAutoAnimateOptions( autoAnimatedParent, options ); | 
 | 321 | 			} | 
 | 322 | 		} | 
 | 323 |  | 
 | 324 | 		if( element.dataset.autoAnimateEasing ) { | 
 | 325 | 			options.easing = element.dataset.autoAnimateEasing; | 
 | 326 | 		} | 
 | 327 |  | 
 | 328 | 		if( element.dataset.autoAnimateDuration ) { | 
 | 329 | 			options.duration = parseFloat( element.dataset.autoAnimateDuration ); | 
 | 330 | 		} | 
 | 331 |  | 
 | 332 | 		if( element.dataset.autoAnimateDelay ) { | 
 | 333 | 			options.delay = parseFloat( element.dataset.autoAnimateDelay ); | 
 | 334 | 		} | 
 | 335 |  | 
 | 336 | 		return options; | 
 | 337 |  | 
 | 338 | 	} | 
 | 339 |  | 
 | 340 | 	/** | 
 | 341 | 	 * Returns an object containing all of the properties | 
 | 342 | 	 * that can be auto-animated for the given element and | 
 | 343 | 	 * their current computed values. | 
 | 344 | 	 * | 
 | 345 | 	 * @param {String} direction 'from' or 'to' | 
 | 346 | 	 */ | 
 | 347 | 	getAutoAnimatableProperties( direction, element, elementOptions ) { | 
 | 348 |  | 
 | 349 | 		let config = this.Reveal.getConfig(); | 
 | 350 |  | 
 | 351 | 		let properties = { styles: [] }; | 
 | 352 |  | 
 | 353 | 		// Position and size | 
 | 354 | 		if( elementOptions.translate !== false || elementOptions.scale !== false ) { | 
 | 355 | 			let bounds; | 
 | 356 |  | 
 | 357 | 			// Custom auto-animate may optionally return a custom tailored | 
 | 358 | 			// measurement function | 
 | 359 | 			if( typeof elementOptions.measure === 'function' ) { | 
 | 360 | 				bounds = elementOptions.measure( element ); | 
 | 361 | 			} | 
 | 362 | 			else { | 
 | 363 | 				if( config.center ) { | 
 | 364 | 					// More precise, but breaks when used in combination | 
 | 365 | 					// with zoom for scaling the deck ¯\_(ツ)_/¯ | 
 | 366 | 					bounds = element.getBoundingClientRect(); | 
 | 367 | 				} | 
 | 368 | 				else { | 
 | 369 | 					let scale = this.Reveal.getScale(); | 
 | 370 | 					bounds = { | 
 | 371 | 						x: element.offsetLeft * scale, | 
 | 372 | 						y: element.offsetTop * scale, | 
 | 373 | 						width: element.offsetWidth * scale, | 
 | 374 | 						height: element.offsetHeight * scale | 
 | 375 | 					}; | 
 | 376 | 				} | 
 | 377 | 			} | 
 | 378 |  | 
 | 379 | 			properties.x = bounds.x; | 
 | 380 | 			properties.y = bounds.y; | 
 | 381 | 			properties.width = bounds.width; | 
 | 382 | 			properties.height = bounds.height; | 
 | 383 | 		} | 
 | 384 |  | 
 | 385 | 		const computedStyles = getComputedStyle( element ); | 
 | 386 |  | 
 | 387 | 		// CSS styles | 
 | 388 | 		( elementOptions.styles || config.autoAnimateStyles ).forEach( style => { | 
 | 389 | 			let value; | 
 | 390 |  | 
 | 391 | 			// `style` is either the property name directly, or an object | 
 | 392 | 			// definition of a style property | 
 | 393 | 			if( typeof style === 'string' ) style = { property: style }; | 
 | 394 |  | 
 | 395 | 			if( typeof style.from !== 'undefined' && direction === 'from' ) { | 
 | 396 | 				value = { value: style.from, explicitValue: true }; | 
 | 397 | 			} | 
 | 398 | 			else if( typeof style.to !== 'undefined' && direction === 'to' ) { | 
 | 399 | 				value = { value: style.to, explicitValue: true }; | 
 | 400 | 			} | 
 | 401 | 			else { | 
| Marc Kupietz | 09b7575 | 2023-10-07 09:32:19 +0200 | [diff] [blame] | 402 | 				// Use a unitless value for line-height so that it inherits properly | 
 | 403 | 				if( style.property === 'line-height' ) { | 
 | 404 | 					value = parseFloat( computedStyles['line-height'] ) / parseFloat( computedStyles['font-size'] ); | 
 | 405 | 				} | 
 | 406 |  | 
 | 407 | 				if( isNaN(value) ) { | 
 | 408 | 					value = computedStyles[style.property]; | 
 | 409 | 				} | 
| Christophe Dervieux | e1893ae | 2021-10-07 17:09:02 +0200 | [diff] [blame] | 410 | 			} | 
 | 411 |  | 
 | 412 | 			if( value !== '' ) { | 
 | 413 | 				properties.styles[style.property] = value; | 
 | 414 | 			} | 
 | 415 | 		} ); | 
 | 416 |  | 
 | 417 | 		return properties; | 
 | 418 |  | 
 | 419 | 	} | 
 | 420 |  | 
 | 421 | 	/** | 
 | 422 | 	 * Get a list of all element pairs that we can animate | 
 | 423 | 	 * between the given slides. | 
 | 424 | 	 * | 
 | 425 | 	 * @param {HTMLElement} fromSlide | 
 | 426 | 	 * @param {HTMLElement} toSlide | 
 | 427 | 	 * | 
 | 428 | 	 * @return {Array} Each value is an array where [0] is | 
 | 429 | 	 * the element we're animating from and [1] is the | 
 | 430 | 	 * element we're animating to | 
 | 431 | 	 */ | 
 | 432 | 	getAutoAnimatableElements( fromSlide, toSlide ) { | 
 | 433 |  | 
 | 434 | 		let matcher = typeof this.Reveal.getConfig().autoAnimateMatcher === 'function' ? this.Reveal.getConfig().autoAnimateMatcher : this.getAutoAnimatePairs; | 
 | 435 |  | 
 | 436 | 		let pairs = matcher.call( this, fromSlide, toSlide ); | 
 | 437 |  | 
 | 438 | 		let reserved = []; | 
 | 439 |  | 
 | 440 | 		// Remove duplicate pairs | 
 | 441 | 		return pairs.filter( ( pair, index ) => { | 
 | 442 | 			if( reserved.indexOf( pair.to ) === -1 ) { | 
 | 443 | 				reserved.push( pair.to ); | 
 | 444 | 				return true; | 
 | 445 | 			} | 
 | 446 | 		} ); | 
 | 447 |  | 
 | 448 | 	} | 
 | 449 |  | 
 | 450 | 	/** | 
 | 451 | 	 * Identifies matching elements between slides. | 
 | 452 | 	 * | 
 | 453 | 	 * You can specify a custom matcher function by using | 
 | 454 | 	 * the `autoAnimateMatcher` config option. | 
 | 455 | 	 */ | 
 | 456 | 	getAutoAnimatePairs( fromSlide, toSlide ) { | 
 | 457 |  | 
 | 458 | 		let pairs = []; | 
 | 459 |  | 
 | 460 | 		const codeNodes = 'pre'; | 
 | 461 | 		const textNodes = 'h1, h2, h3, h4, h5, h6, p, li'; | 
 | 462 | 		const mediaNodes = 'img, video, iframe'; | 
 | 463 |  | 
| Marc Kupietz | 09b7575 | 2023-10-07 09:32:19 +0200 | [diff] [blame] | 464 | 		// Explicit matches via data-id | 
| Christophe Dervieux | e1893ae | 2021-10-07 17:09:02 +0200 | [diff] [blame] | 465 | 		this.findAutoAnimateMatches( pairs, fromSlide, toSlide, '[data-id]', node => { | 
 | 466 | 			return node.nodeName + ':::' + node.getAttribute( 'data-id' ); | 
 | 467 | 		} ); | 
 | 468 |  | 
 | 469 | 		// Text | 
 | 470 | 		this.findAutoAnimateMatches( pairs, fromSlide, toSlide, textNodes, node => { | 
 | 471 | 			return node.nodeName + ':::' + node.innerText; | 
 | 472 | 		} ); | 
 | 473 |  | 
 | 474 | 		// Media | 
 | 475 | 		this.findAutoAnimateMatches( pairs, fromSlide, toSlide, mediaNodes, node => { | 
 | 476 | 			return node.nodeName + ':::' + ( node.getAttribute( 'src' ) || node.getAttribute( 'data-src' ) ); | 
 | 477 | 		} ); | 
 | 478 |  | 
 | 479 | 		// Code | 
 | 480 | 		this.findAutoAnimateMatches( pairs, fromSlide, toSlide, codeNodes, node => { | 
 | 481 | 			return node.nodeName + ':::' + node.innerText; | 
 | 482 | 		} ); | 
 | 483 |  | 
 | 484 | 		pairs.forEach( pair => { | 
| Christophe Dervieux | e1893ae | 2021-10-07 17:09:02 +0200 | [diff] [blame] | 485 | 			// Disable scale transformations on text nodes, we transition | 
 | 486 | 			// each individual text property instead | 
 | 487 | 			if( matches( pair.from, textNodes ) ) { | 
 | 488 | 				pair.options = { scale: false }; | 
 | 489 | 			} | 
 | 490 | 			// Animate individual lines of code | 
 | 491 | 			else if( matches( pair.from, codeNodes ) ) { | 
 | 492 |  | 
 | 493 | 				// Transition the code block's width and height instead of scaling | 
 | 494 | 				// to prevent its content from being squished | 
 | 495 | 				pair.options = { scale: false, styles: [ 'width', 'height' ] }; | 
 | 496 |  | 
 | 497 | 				// Lines of code | 
 | 498 | 				this.findAutoAnimateMatches( pairs, pair.from, pair.to, '.hljs .hljs-ln-code', node => { | 
 | 499 | 					return node.textContent; | 
 | 500 | 				}, { | 
 | 501 | 					scale: false, | 
 | 502 | 					styles: [], | 
 | 503 | 					measure: this.getLocalBoundingBox.bind( this ) | 
 | 504 | 				} ); | 
 | 505 |  | 
 | 506 | 				// Line numbers | 
| Marc Kupietz | 09b7575 | 2023-10-07 09:32:19 +0200 | [diff] [blame] | 507 | 				this.findAutoAnimateMatches( pairs, pair.from, pair.to, '.hljs .hljs-ln-numbers[data-line-number]', node => { | 
| Christophe Dervieux | e1893ae | 2021-10-07 17:09:02 +0200 | [diff] [blame] | 508 | 					return node.getAttribute( 'data-line-number' ); | 
 | 509 | 				}, { | 
 | 510 | 					scale: false, | 
 | 511 | 					styles: [ 'width' ], | 
 | 512 | 					measure: this.getLocalBoundingBox.bind( this ) | 
 | 513 | 				} ); | 
 | 514 |  | 
 | 515 | 			} | 
 | 516 |  | 
 | 517 | 		}, this ); | 
 | 518 |  | 
 | 519 | 		return pairs; | 
 | 520 |  | 
 | 521 | 	} | 
 | 522 |  | 
 | 523 | 	/** | 
 | 524 | 	 * Helper method which returns a bounding box based on | 
 | 525 | 	 * the given elements offset coordinates. | 
 | 526 | 	 * | 
 | 527 | 	 * @param {HTMLElement} element | 
 | 528 | 	 * @return {Object} x, y, width, height | 
 | 529 | 	 */ | 
 | 530 | 	getLocalBoundingBox( element ) { | 
 | 531 |  | 
 | 532 | 		const presentationScale = this.Reveal.getScale(); | 
 | 533 |  | 
 | 534 | 		return { | 
 | 535 | 			x: Math.round( ( element.offsetLeft * presentationScale ) * 100 ) / 100, | 
 | 536 | 			y: Math.round( ( element.offsetTop * presentationScale ) * 100 ) / 100, | 
 | 537 | 			width: Math.round( ( element.offsetWidth * presentationScale ) * 100 ) / 100, | 
 | 538 | 			height: Math.round( ( element.offsetHeight * presentationScale ) * 100 ) / 100 | 
 | 539 | 		}; | 
 | 540 |  | 
 | 541 | 	} | 
 | 542 |  | 
 | 543 | 	/** | 
 | 544 | 	 * Finds matching elements between two slides. | 
 | 545 | 	 * | 
 | 546 | 	 * @param {Array} pairs            	List of pairs to push matches to | 
 | 547 | 	 * @param {HTMLElement} fromScope   Scope within the from element exists | 
 | 548 | 	 * @param {HTMLElement} toScope     Scope within the to element exists | 
 | 549 | 	 * @param {String} selector         CSS selector of the element to match | 
 | 550 | 	 * @param {Function} serializer     A function that accepts an element and returns | 
 | 551 | 	 *                                  a stringified ID based on its contents | 
 | 552 | 	 * @param {Object} animationOptions Optional config options for this pair | 
 | 553 | 	 */ | 
 | 554 | 	findAutoAnimateMatches( pairs, fromScope, toScope, selector, serializer, animationOptions ) { | 
 | 555 |  | 
 | 556 | 		let fromMatches = {}; | 
 | 557 | 		let toMatches = {}; | 
 | 558 |  | 
 | 559 | 		[].slice.call( fromScope.querySelectorAll( selector ) ).forEach( ( element, i ) => { | 
 | 560 | 			const key = serializer( element ); | 
 | 561 | 			if( typeof key === 'string' && key.length ) { | 
 | 562 | 				fromMatches[key] = fromMatches[key] || []; | 
 | 563 | 				fromMatches[key].push( element ); | 
 | 564 | 			} | 
 | 565 | 		} ); | 
 | 566 |  | 
 | 567 | 		[].slice.call( toScope.querySelectorAll( selector ) ).forEach( ( element, i ) => { | 
 | 568 | 			const key = serializer( element ); | 
 | 569 | 			toMatches[key] = toMatches[key] || []; | 
 | 570 | 			toMatches[key].push( element ); | 
 | 571 |  | 
 | 572 | 			let fromElement; | 
 | 573 |  | 
 | 574 | 			// Retrieve the 'from' element | 
 | 575 | 			if( fromMatches[key] ) { | 
| Marc Kupietz | 09b7575 | 2023-10-07 09:32:19 +0200 | [diff] [blame] | 576 | 				const primaryIndex = toMatches[key].length - 1; | 
| Christophe Dervieux | e1893ae | 2021-10-07 17:09:02 +0200 | [diff] [blame] | 577 | 				const secondaryIndex = fromMatches[key].length - 1; | 
 | 578 |  | 
 | 579 | 				// If there are multiple identical from elements, retrieve | 
 | 580 | 				// the one at the same index as our to-element. | 
| Marc Kupietz | 09b7575 | 2023-10-07 09:32:19 +0200 | [diff] [blame] | 581 | 				if( fromMatches[key][ primaryIndex ] ) { | 
 | 582 | 					fromElement = fromMatches[key][ primaryIndex ]; | 
 | 583 | 					fromMatches[key][ primaryIndex ] = null; | 
| Christophe Dervieux | e1893ae | 2021-10-07 17:09:02 +0200 | [diff] [blame] | 584 | 				} | 
 | 585 | 				// If there are no matching from-elements at the same index, | 
 | 586 | 				// use the last one. | 
 | 587 | 				else if( fromMatches[key][ secondaryIndex ] ) { | 
 | 588 | 					fromElement = fromMatches[key][ secondaryIndex ]; | 
 | 589 | 					fromMatches[key][ secondaryIndex ] = null; | 
 | 590 | 				} | 
 | 591 | 			} | 
 | 592 |  | 
 | 593 | 			// If we've got a matching pair, push it to the list of pairs | 
 | 594 | 			if( fromElement ) { | 
 | 595 | 				pairs.push({ | 
 | 596 | 					from: fromElement, | 
 | 597 | 					to: element, | 
 | 598 | 					options: animationOptions | 
 | 599 | 				}); | 
 | 600 | 			} | 
 | 601 | 		} ); | 
 | 602 |  | 
 | 603 | 	} | 
 | 604 |  | 
 | 605 | 	/** | 
 | 606 | 	 * Returns a all elements within the given scope that should | 
 | 607 | 	 * be considered unmatched in an auto-animate transition. If | 
 | 608 | 	 * fading of unmatched elements is turned on, these elements | 
 | 609 | 	 * will fade when going between auto-animate slides. | 
 | 610 | 	 * | 
| Marc Kupietz | 09b7575 | 2023-10-07 09:32:19 +0200 | [diff] [blame] | 611 | 	 * Note that parents of auto-animate targets are NOT considered | 
| Christophe Dervieux | e1893ae | 2021-10-07 17:09:02 +0200 | [diff] [blame] | 612 | 	 * unmatched since fading them would break the auto-animation. | 
 | 613 | 	 * | 
 | 614 | 	 * @param {HTMLElement} rootElement | 
 | 615 | 	 * @return {Array} | 
 | 616 | 	 */ | 
 | 617 | 	getUnmatchedAutoAnimateElements( rootElement ) { | 
 | 618 |  | 
 | 619 | 		return [].slice.call( rootElement.children ).reduce( ( result, element ) => { | 
 | 620 |  | 
 | 621 | 			const containsAnimatedElements = element.querySelector( '[data-auto-animate-target]' ); | 
 | 622 |  | 
 | 623 | 			// The element is unmatched if | 
 | 624 | 			// - It is not an auto-animate target | 
 | 625 | 			// - It does not contain any auto-animate targets | 
 | 626 | 			if( !element.hasAttribute( 'data-auto-animate-target' ) && !containsAnimatedElements ) { | 
 | 627 | 				result.push( element ); | 
 | 628 | 			} | 
 | 629 |  | 
 | 630 | 			if( element.querySelector( '[data-auto-animate-target]' ) ) { | 
 | 631 | 				result = result.concat( this.getUnmatchedAutoAnimateElements( element ) ); | 
 | 632 | 			} | 
 | 633 |  | 
 | 634 | 			return result; | 
 | 635 |  | 
 | 636 | 		}, [] ); | 
 | 637 |  | 
 | 638 | 	} | 
 | 639 |  | 
 | 640 | } |