add support for automatically scrolling code highlights into view
							parent
							
								
									5a5a5c9a6c
								
							
						
					
					
						commit
						bff9bfb101
					
				|  | @ -39,6 +39,7 @@ body { | |||
| 	opacity: 0; | ||||
| 	visibility: hidden; | ||||
| 	transition: all .2s ease; | ||||
| 	will-change: opacity; | ||||
| 
 | ||||
| 	&.visible { | ||||
| 		opacity: 1; | ||||
|  | @ -1599,6 +1600,10 @@ $overlayHeaderPadding: 5px; | |||
|  * CODE HIGHLGIHTING | ||||
|  *********************************************/ | ||||
| 
 | ||||
| .reveal .hljs { | ||||
| 	min-height: 100%; | ||||
| } | ||||
| 
 | ||||
| .reveal .hljs table { | ||||
| 	margin: initial; | ||||
| } | ||||
|  |  | |||
							
								
								
									
										15
									
								
								demo.html
								
								
								
								
							
							
						
						
									
										15
									
								
								demo.html
								
								
								
								
							|  | @ -102,7 +102,7 @@ | |||
| 
 | ||||
| 				<section data-auto-animate> | ||||
| 					<h2 data-id="code-title">With animations</h2> | ||||
| 					<pre data-id="code-animation"><code class="hljs" data-trim data-line-numbers="|4|4,8-11"> | ||||
| 					<pre data-id="code-animation"><code class="hljs" data-trim data-line-numbers="|4,8-11|17|22-24"> | ||||
| 						import React, { useState } from 'react'; | ||||
| 
 | ||||
| 						function Example() { | ||||
|  | @ -117,6 +117,19 @@ | |||
| 						    </div> | ||||
| 						  ); | ||||
| 						} | ||||
| 
 | ||||
| 						function SecondExample() { | ||||
| 						  const [count, setCount] = useState(0); | ||||
| 
 | ||||
| 						  return ( | ||||
| 						    <div> | ||||
| 						      <p>You clicked {count} times</p> | ||||
| 						      <button onClick={() => setCount(count + 1)}> | ||||
| 						        Click me | ||||
| 						      </button> | ||||
| 						    </div> | ||||
| 						  ); | ||||
| 						} | ||||
| 					</code></pre> | ||||
| 				</section> | ||||
| 
 | ||||
|  |  | |||
|  | @ -67,7 +67,14 @@ export default class AutoAnimate { | |||
| 				} | ||||
| 			} ); | ||||
| 
 | ||||
| 			this.Reveal.dispatchEvent( 'autoanimate', { fromSlide: fromSlide, toSlide: toSlide, sheet: this.autoAnimateStyleSheet } ); | ||||
| 			this.Reveal.dispatchEvent({ | ||||
| 				type: 'autoanimate', | ||||
| 				data: { | ||||
| 					fromSlide, | ||||
| 					toSlide, | ||||
| 					sheet: this.autoAnimateStyleSheet | ||||
| 				} | ||||
| 			}); | ||||
| 
 | ||||
| 		} | ||||
| 
 | ||||
|  |  | |||
|  | @ -180,7 +180,7 @@ export default class Fragments { | |||
| 
 | ||||
| 					// Visible fragments
 | ||||
| 					if( i <= index ) { | ||||
| 						if( !el.classList.contains( 'visible' ) ) changedFragments.shown.push( el ); | ||||
| 						let wasVisible = el.classList.contains( 'visible' ) | ||||
| 						el.classList.add( 'visible' ); | ||||
| 						el.classList.remove( 'current-fragment' ); | ||||
| 
 | ||||
|  | @ -191,12 +191,30 @@ export default class Fragments { | |||
| 							el.classList.add( 'current-fragment' ); | ||||
| 							this.Reveal.slideContent.startEmbeddedContent( el ); | ||||
| 						} | ||||
| 
 | ||||
| 						if( !wasVisible ) { | ||||
| 							changedFragments.shown.push( el ) | ||||
| 							this.Reveal.dispatchEvent({ | ||||
| 								target: el, | ||||
| 								type: 'visible', | ||||
| 								bubbles: false | ||||
| 							}); | ||||
| 						} | ||||
| 					} | ||||
| 					// Hidden fragments
 | ||||
| 					else { | ||||
| 						if( el.classList.contains( 'visible' ) ) changedFragments.hidden.push( el ); | ||||
| 						let wasVisible = el.classList.contains( 'visible' ) | ||||
| 						el.classList.remove( 'visible' ); | ||||
| 						el.classList.remove( 'current-fragment' ); | ||||
| 
 | ||||
| 						if( wasVisible ) { | ||||
| 							changedFragments.hidden.push( el ); | ||||
| 							this.Reveal.dispatchEvent({ | ||||
| 								target: el, | ||||
| 								type: 'hidden', | ||||
| 								bubbles: false | ||||
| 							}); | ||||
| 						} | ||||
| 					} | ||||
| 
 | ||||
| 				} ); | ||||
|  | @ -253,11 +271,23 @@ export default class Fragments { | |||
| 				let changedFragments = this.update( index, fragments ); | ||||
| 
 | ||||
| 				if( changedFragments.hidden.length ) { | ||||
| 					this.Reveal.dispatchEvent( 'fragmenthidden', { fragment: changedFragments.hidden[0], fragments: changedFragments.hidden } ); | ||||
| 					this.Reveal.dispatchEvent({ | ||||
| 						type: 'fragmenthidden', | ||||
| 						data: { | ||||
| 							fragment: changedFragments.hidden[0], | ||||
| 							fragments: changedFragments.hidden | ||||
| 						} | ||||
| 					}); | ||||
| 				} | ||||
| 
 | ||||
| 				if( changedFragments.shown.length ) { | ||||
| 					this.Reveal.dispatchEvent( 'fragmentshown', { fragment: changedFragments.shown[0], fragments: changedFragments.shown } ); | ||||
| 					this.Reveal.dispatchEvent({ | ||||
| 						type: 'fragmentshown', | ||||
| 						data: { | ||||
| 							fragment: changedFragments.shown[0], | ||||
| 							fragments: changedFragments.shown | ||||
| 						} | ||||
| 					}); | ||||
| 				} | ||||
| 
 | ||||
| 				this.Reveal.updateControls(); | ||||
|  |  | |||
|  | @ -65,11 +65,14 @@ export default class Overview { | |||
| 			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() | ||||
| 			} ); | ||||
| 			this.Reveal.dispatchEvent({ | ||||
| 				type: 'overviewshown', | ||||
| 				data: { | ||||
| 					'indexh': indices.h, | ||||
| 					'indexv': indices.v, | ||||
| 					'currentSlide': this.Reveal.getCurrentSlide() | ||||
| 				} | ||||
| 			}); | ||||
| 
 | ||||
| 		} | ||||
| 
 | ||||
|  | @ -175,11 +178,14 @@ export default class Overview { | |||
| 			this.Reveal.cueAutoSlide(); | ||||
| 
 | ||||
| 			// Notify observers of the overview hiding
 | ||||
| 			this.Reveal.dispatchEvent( 'overviewhidden', { | ||||
| 				'indexh': indices.h, | ||||
| 				'indexv': indices.v, | ||||
| 				'currentSlide': this.Reveal.getCurrentSlide() | ||||
| 			} ); | ||||
| 			this.Reveal.dispatchEvent({ | ||||
| 				type: 'overviewhidden', | ||||
| 				data: { | ||||
| 					'indexh': indices.h, | ||||
| 					'indexv': indices.v, | ||||
| 					'currentSlide': this.Reveal.getCurrentSlide() | ||||
| 				} | ||||
| 			}); | ||||
| 
 | ||||
| 		} | ||||
| 	} | ||||
|  |  | |||
							
								
								
									
										91
									
								
								js/reveal.js
								
								
								
								
							
							
						
						
									
										91
									
								
								js/reveal.js
								
								
								
								
							|  | @ -194,11 +194,14 @@ export default function( revealElement, options ) { | |||
| 
 | ||||
| 			dom.wrapper.classList.add( 'ready' ); | ||||
| 
 | ||||
| 			dispatchEvent( 'ready', { | ||||
| 				'indexh': indexh, | ||||
| 				'indexv': indexv, | ||||
| 				'currentSlide': currentSlide | ||||
| 			} ); | ||||
| 			dispatchEvent({ | ||||
| 				type: 'ready', | ||||
| 				data: { | ||||
| 					indexh, | ||||
| 					indexv, | ||||
| 					currentSlide | ||||
| 				} | ||||
| 			}); | ||||
| 		}, 1 ); | ||||
| 
 | ||||
| 		// Special setup and config is required when printing to PDF
 | ||||
|  | @ -511,7 +514,7 @@ export default function( revealElement, options ) { | |||
| 		} ); | ||||
| 
 | ||||
| 		// Notify subscribers that the PDF layout is good to go
 | ||||
| 		dispatchEvent( 'pdf-ready' ); | ||||
| 		dispatchEvent({ type: 'pdf-ready' }); | ||||
| 
 | ||||
| 	} | ||||
| 
 | ||||
|  | @ -1058,16 +1061,18 @@ export default function( revealElement, options ) { | |||
| 	 * Dispatches an event of the specified type from the | ||||
| 	 * reveal DOM element. | ||||
| 	 */ | ||||
| 	function dispatchEvent( type, args ) { | ||||
| 	function dispatchEvent({ target=dom.wrapper, type, data, bubbles=true }) { | ||||
| 
 | ||||
| 		let event = document.createEvent( 'HTMLEvents', 1, 2 ); | ||||
| 		event.initEvent( type, true, true ); | ||||
| 		extend( event, args ); | ||||
| 		dom.wrapper.dispatchEvent( event ); | ||||
| 		event.initEvent( type, bubbles, true ); | ||||
| 		extend( event, data ); | ||||
| 		target.dispatchEvent( event ); | ||||
| 
 | ||||
| 		// If we're in an iframe, post each reveal.js event to the
 | ||||
| 		// parent window. Used by the notes plugin
 | ||||
| 		dispatchPostMessage( type ); | ||||
| 		if( target === dom.wrapper ) { | ||||
| 			// If we're in an iframe, post each reveal.js event to the
 | ||||
| 			// parent window. Used by the notes plugin
 | ||||
| 			dispatchPostMessage( type ); | ||||
| 		} | ||||
| 
 | ||||
| 	} | ||||
| 
 | ||||
|  | @ -1347,11 +1352,14 @@ export default function( revealElement, options ) { | |||
| 				} | ||||
| 
 | ||||
| 				if( oldScale !== scale ) { | ||||
| 					dispatchEvent( 'resize', { | ||||
| 						'oldScale': oldScale, | ||||
| 						'scale': scale, | ||||
| 						'size': size | ||||
| 					} ); | ||||
| 					dispatchEvent({ | ||||
| 						type: 'resize', | ||||
| 						data: { | ||||
| 							oldScale, | ||||
| 							scale, | ||||
| 							size | ||||
| 						} | ||||
| 					}); | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
|  | @ -1577,7 +1585,7 @@ export default function( revealElement, options ) { | |||
| 			dom.wrapper.classList.add( 'paused' ); | ||||
| 
 | ||||
| 			if( wasPaused === false ) { | ||||
| 				dispatchEvent( 'paused' ); | ||||
| 				dispatchEvent({ type: 'paused' }); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
|  | @ -1594,7 +1602,7 @@ export default function( revealElement, options ) { | |||
| 		cueAutoSlide(); | ||||
| 
 | ||||
| 		if( wasPaused ) { | ||||
| 			dispatchEvent( 'resumed' ); | ||||
| 			dispatchEvent({ type: 'resumed' }); | ||||
| 		} | ||||
| 
 | ||||
| 	} | ||||
|  | @ -1763,7 +1771,7 @@ export default function( revealElement, options ) { | |||
| 			document.documentElement.classList.add( state[i] ); | ||||
| 
 | ||||
| 			// Dispatch custom event matching the state's name
 | ||||
| 			dispatchEvent( state[i] ); | ||||
| 			dispatchEvent({ type: state[i] }); | ||||
| 		} | ||||
| 
 | ||||
| 		// Clean up the remains of the previous state
 | ||||
|  | @ -1772,13 +1780,16 @@ export default function( revealElement, options ) { | |||
| 		} | ||||
| 
 | ||||
| 		if( slideChanged ) { | ||||
| 			dispatchEvent( 'slidechanged', { | ||||
| 				'indexh': indexh, | ||||
| 				'indexv': indexv, | ||||
| 				'previousSlide': previousSlide, | ||||
| 				'currentSlide': currentSlide, | ||||
| 				'origin': o | ||||
| 			} ); | ||||
| 			dispatchEvent({ | ||||
| 				type: 'slidechanged', | ||||
| 				data: { | ||||
| 					indexh, | ||||
| 					indexv, | ||||
| 					previousSlide, | ||||
| 					currentSlide, | ||||
| 					origin: o | ||||
| 				} | ||||
| 			}); | ||||
| 		} | ||||
| 
 | ||||
| 		// Handle embedded content
 | ||||
|  | @ -2035,14 +2046,26 @@ export default function( revealElement, options ) { | |||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			let slide = slides[index]; | ||||
| 			let wasPresent = slide.classList.contains( 'present' ); | ||||
| 
 | ||||
| 			// Mark the current slide as present
 | ||||
| 			slides[index].classList.add( 'present' ); | ||||
| 			slides[index].removeAttribute( 'hidden' ); | ||||
| 			slides[index].removeAttribute( 'aria-hidden' ); | ||||
| 			slide.classList.add( 'present' ); | ||||
| 			slide.removeAttribute( 'hidden' ); | ||||
| 			slide.removeAttribute( 'aria-hidden' ); | ||||
| 
 | ||||
| 			if( !wasPresent ) { | ||||
| 				// Dispatch an event indicating the slide is now visible
 | ||||
| 				dispatchEvent({ | ||||
| 					target: slide, | ||||
| 					type: 'visible', | ||||
| 					bubbles: false | ||||
| 				}); | ||||
| 			} | ||||
| 
 | ||||
| 			// If this slide has a state associated with it, add it
 | ||||
| 			// onto the current state of the deck
 | ||||
| 			let slideState = slides[index].getAttribute( 'data-state' ); | ||||
| 			let slideState = slide.getAttribute( 'data-state' ); | ||||
| 			if( slideState ) { | ||||
| 				state = state.concat( slideState.split( ' ' ) ); | ||||
| 			} | ||||
|  | @ -2947,7 +2970,7 @@ export default function( revealElement, options ) { | |||
| 
 | ||||
| 		if( autoSlide && !autoSlidePaused ) { | ||||
| 			autoSlidePaused = true; | ||||
| 			dispatchEvent( 'autoslidepaused' ); | ||||
| 			dispatchEvent({ type: 'autoslidepaused' }); | ||||
| 			clearTimeout( autoSlideTimeout ); | ||||
| 
 | ||||
| 			if( autoSlidePlayer ) { | ||||
|  | @ -2961,7 +2984,7 @@ export default function( revealElement, options ) { | |||
| 
 | ||||
| 		if( autoSlide && autoSlidePaused ) { | ||||
| 			autoSlidePaused = false; | ||||
| 			dispatchEvent( 'autoslideresumed' ); | ||||
| 			dispatchEvent({ type: 'autoslideresumed' }); | ||||
| 			cueAutoSlide(); | ||||
| 		} | ||||
| 
 | ||||
|  |  | |||
|  | @ -100,6 +100,15 @@ | |||
| 				if( config.highlightOnLoad ) { | ||||
| 					RevealHighlight.highlightBlock( block ); | ||||
| 				} | ||||
| 
 | ||||
| 			} ); | ||||
| 
 | ||||
| 			// If we're printing to PDF, scroll the code highlights of
 | ||||
| 			// all blocks in the deck into view at once
 | ||||
| 			Reveal.addEventListener( 'pdf-ready', function() { | ||||
| 				[].slice.call( document.querySelectorAll( '.reveal pre code[data-line-numbers].current-fragment' ) ).forEach( function( block ) { | ||||
| 					RevealHighlight.scrollHighlightedLineIntoView( block, {}, true ); | ||||
| 				} ); | ||||
| 			} ); | ||||
| 
 | ||||
| 		}, | ||||
|  | @ -122,6 +131,8 @@ | |||
| 			if( block.hasAttribute( 'data-line-numbers' ) ) { | ||||
| 				hljs.lineNumbersBlock( block, { singleLine: true } ); | ||||
| 
 | ||||
| 				var scrollState = { currentBlock: block }; | ||||
| 
 | ||||
| 				// If there is at least one highlight step, generate
 | ||||
| 				// fragments
 | ||||
| 				var highlightSteps = RevealHighlight.deserializeHighlightSteps( block.getAttribute( 'data-line-numbers' ) ); | ||||
|  | @ -130,6 +141,7 @@ | |||
| 					// If the original code block has a fragment-index,
 | ||||
| 					// each clone should follow in an incremental sequence
 | ||||
| 					var fragmentIndex = parseInt( block.getAttribute( 'data-fragment-index' ), 10 ); | ||||
| 
 | ||||
| 					if( typeof fragmentIndex !== 'number' || isNaN( fragmentIndex ) ) { | ||||
| 						fragmentIndex = null; | ||||
| 					} | ||||
|  | @ -151,6 +163,10 @@ | |||
| 							fragmentBlock.removeAttribute( 'data-fragment-index' ); | ||||
| 						} | ||||
| 
 | ||||
| 						// Scroll highlights into view as we step through them
 | ||||
| 						fragmentBlock.addEventListener( 'visible', RevealHighlight.scrollHighlightedLineIntoView.bind( RevealHighlight, fragmentBlock, scrollState ) ); | ||||
| 						fragmentBlock.addEventListener( 'hidden', RevealHighlight.scrollHighlightedLineIntoView.bind( RevealHighlight, fragmentBlock.previousSibling, scrollState ) ); | ||||
| 
 | ||||
| 					} ); | ||||
| 
 | ||||
| 					block.removeAttribute( 'data-fragment-index' ) | ||||
|  | @ -158,12 +174,116 @@ | |||
| 
 | ||||
| 				} | ||||
| 
 | ||||
| 				// Scroll the first highlight into view when the slide
 | ||||
| 				// becomes visible. Note supported in IE11 since it lacks
 | ||||
| 				// support for Element.closest.
 | ||||
| 				var slide = typeof block.closest === 'function' ? block.closest( 'section:not(.stack)' ) : null; | ||||
| 				if( slide ) { | ||||
| 					var scrollFirstHighlightIntoView = function() { | ||||
| 						RevealHighlight.scrollHighlightedLineIntoView( block, scrollState, true ); | ||||
| 						slide.removeEventListener( 'visible', scrollFirstHighlightIntoView ); | ||||
| 					} | ||||
| 					slide.addEventListener( 'visible', scrollFirstHighlightIntoView ); | ||||
| 				} | ||||
| 
 | ||||
| 				RevealHighlight.highlightLines( block ); | ||||
| 
 | ||||
| 			} | ||||
| 
 | ||||
| 		}, | ||||
| 
 | ||||
| 		/** | ||||
| 		 * Animates scrolling to the first highlighted line | ||||
| 		 * in the given code block. | ||||
| 		 */ | ||||
| 		scrollHighlightedLineIntoView: function( block, scrollState, skipAnimation ) { | ||||
| 
 | ||||
| 			cancelAnimationFrame( scrollState.animationFrameID ); | ||||
| 
 | ||||
| 			// Match the scroll position of the currently visible
 | ||||
| 			// code block
 | ||||
| 			if( scrollState.currentBlock ) { | ||||
| 				block.scrollTop = scrollState.currentBlock.scrollTop; | ||||
| 			} | ||||
| 
 | ||||
| 			// Remember the current code block so that we can match
 | ||||
| 			// its scroll position when showing/hiding fragments
 | ||||
| 			scrollState.currentBlock = block; | ||||
| 
 | ||||
| 			var highlightBounds = this.getHighlightedLineBounds( block ) | ||||
| 			var viewportHeight = block.offsetHeight; | ||||
| 
 | ||||
| 			// Subtract padding from the viewport height
 | ||||
| 			var blockStyles = getComputedStyle( block ); | ||||
| 			viewportHeight -= parseInt( blockStyles.paddingTop ) + parseInt( blockStyles.paddingBottom ); | ||||
| 
 | ||||
| 			// Scroll position which centers all highlights
 | ||||
| 			var startTop = block.scrollTop; | ||||
| 			var targetTop = highlightBounds.top + ( Math.min( highlightBounds.bottom - highlightBounds.top, viewportHeight ) - viewportHeight ) / 2; | ||||
| 
 | ||||
| 			// Account for offsets in position applied to the
 | ||||
| 			// <table> that holds our lines of code
 | ||||
| 			var lineTable = block.querySelector( '.hljs-ln' ); | ||||
| 			if( lineTable ) targetTop += lineTable.offsetTop - parseInt( blockStyles.paddingTop ); | ||||
| 
 | ||||
| 			// Make sure the scroll target is within bounds
 | ||||
| 			targetTop = Math.max( Math.min( targetTop, block.scrollHeight - viewportHeight ), 0 ); | ||||
| 
 | ||||
| 			if( skipAnimation === true || startTop === targetTop ) { | ||||
| 				block.scrollTop = targetTop; | ||||
| 			} | ||||
| 			else { | ||||
| 
 | ||||
| 				// Don't attempt to scroll if there is no overflow
 | ||||
| 				if( block.scrollHeight <= viewportHeight ) return; | ||||
| 
 | ||||
| 				var time = 0; | ||||
| 				var animate = function() { | ||||
| 					time = Math.min( time + 0.02, 1 ); | ||||
| 
 | ||||
| 					// Update our eased scroll position
 | ||||
| 					block.scrollTop = startTop + ( targetTop - startTop ) * RevealHighlight.easeInOutQuart( time ); | ||||
| 
 | ||||
| 					// Keep animating unless we've reached the end
 | ||||
| 					if( time < 1 ) { | ||||
| 						scrollState.animationFrameID = requestAnimationFrame( animate ); | ||||
| 					} | ||||
| 				}; | ||||
| 
 | ||||
| 				animate(); | ||||
| 
 | ||||
| 			} | ||||
| 
 | ||||
| 		}, | ||||
| 
 | ||||
| 		/** | ||||
| 		 * The easing function used when scrolling. | ||||
| 		 */ | ||||
| 		easeInOutQuart: function( t ) { | ||||
| 
 | ||||
| 			// easeInOutQuart
 | ||||
| 			return t<.5 ? 8*t*t*t*t : 1-8*(--t)*t*t*t; | ||||
| 
 | ||||
| 		}, | ||||
| 
 | ||||
| 		getHighlightedLineBounds: function( block ) { | ||||
| 
 | ||||
| 			var highlightedLines = block.querySelectorAll( '.highlight-line' ); | ||||
| 			if( highlightedLines.length === 0 ) { | ||||
| 				return { top: 0, bottom: 0 }; | ||||
| 			} | ||||
| 			else { | ||||
| 				var firstHighlight = highlightedLines[0]; | ||||
| 				var lastHighlight = highlightedLines[ highlightedLines.length -1 ]; | ||||
| 
 | ||||
| 				return { | ||||
| 					top: firstHighlight.offsetTop, | ||||
| 					bottom: lastHighlight.offsetTop + lastHighlight.offsetHeight | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 		}, | ||||
| 
 | ||||
| 		/** | ||||
| 		 * Visually emphasize specific lines within a code block. | ||||
| 		 * This only works on blocks with line numbering turned on. | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 Hakim El Hattab
						Hakim El Hattab