diff --git a/css/reveal.scss b/css/reveal.scss index f8803b4b..063aa151 100644 --- a/css/reveal.scss +++ b/css/reveal.scss @@ -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; } diff --git a/demo.html b/demo.html index 8eda9eac..06be8caf 100644 --- a/demo.html +++ b/demo.html @@ -102,7 +102,7 @@

With animations

-

+					

 						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>
+						  );
+						}
 					
diff --git a/js/controllers/autoanimate.js b/js/controllers/autoanimate.js index 9204524c..4b006d42 100644 --- a/js/controllers/autoanimate.js +++ b/js/controllers/autoanimate.js @@ -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 + } + }); } diff --git a/js/controllers/fragments.js b/js/controllers/fragments.js index 0ad699bd..01db85f4 100644 --- a/js/controllers/fragments.js +++ b/js/controllers/fragments.js @@ -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(); diff --git a/js/controllers/overview.js b/js/controllers/overview.js index 08a24045..ce48a069 100644 --- a/js/controllers/overview.js +++ b/js/controllers/overview.js @@ -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() + } + }); } } diff --git a/js/reveal.js b/js/reveal.js index 8d237652..2ece9617 100644 --- a/js/reveal.js +++ b/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(); } diff --git a/plugin/highlight/highlight.js b/plugin/highlight/highlight.js index e751fb7a..494f42e4 100644 --- a/plugin/highlight/highlight.js +++ b/plugin/highlight/highlight.js @@ -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 + // 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.