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.