add support for automatically scrolling code highlights into view

This commit is contained in:
Hakim El Hattab 2020-03-12 17:08:20 +01:00
parent 5a5a5c9a6c
commit bff9bfb101
7 changed files with 254 additions and 50 deletions

View file

@ -39,6 +39,7 @@ body {
opacity: 0; opacity: 0;
visibility: hidden; visibility: hidden;
transition: all .2s ease; transition: all .2s ease;
will-change: opacity;
&.visible { &.visible {
opacity: 1; opacity: 1;
@ -1599,6 +1600,10 @@ $overlayHeaderPadding: 5px;
* CODE HIGHLGIHTING * CODE HIGHLGIHTING
*********************************************/ *********************************************/
.reveal .hljs {
min-height: 100%;
}
.reveal .hljs table { .reveal .hljs table {
margin: initial; margin: initial;
} }

View file

@ -102,7 +102,7 @@
<section data-auto-animate> <section data-auto-animate>
<h2 data-id="code-title">With animations</h2> <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'; import React, { useState } from 'react';
function Example() { function Example() {
@ -117,6 +117,19 @@
&lt;/div&gt; &lt;/div&gt;
); );
} }
function SecondExample() {
const [count, setCount] = useState(0);
return (
&lt;div&gt;
&lt;p&gt;You clicked {count} times&lt;/p&gt;
&lt;button onClick={() =&gt; setCount(count + 1)}&gt;
Click me
&lt;/button&gt;
&lt;/div&gt;
);
}
</code></pre> </code></pre>
</section> </section>

View file

@ -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
}
});
} }

View file

@ -180,7 +180,7 @@ export default class Fragments {
// Visible fragments // Visible fragments
if( i <= index ) { if( i <= index ) {
if( !el.classList.contains( 'visible' ) ) changedFragments.shown.push( el ); let wasVisible = el.classList.contains( 'visible' )
el.classList.add( 'visible' ); el.classList.add( 'visible' );
el.classList.remove( 'current-fragment' ); el.classList.remove( 'current-fragment' );
@ -191,12 +191,30 @@ export default class Fragments {
el.classList.add( 'current-fragment' ); el.classList.add( 'current-fragment' );
this.Reveal.slideContent.startEmbeddedContent( el ); this.Reveal.slideContent.startEmbeddedContent( el );
} }
if( !wasVisible ) {
changedFragments.shown.push( el )
this.Reveal.dispatchEvent({
target: el,
type: 'visible',
bubbles: false
});
}
} }
// Hidden fragments // Hidden fragments
else { else {
if( el.classList.contains( 'visible' ) ) changedFragments.hidden.push( el ); let wasVisible = el.classList.contains( 'visible' )
el.classList.remove( 'visible' ); el.classList.remove( 'visible' );
el.classList.remove( 'current-fragment' ); 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 ); let changedFragments = this.update( index, fragments );
if( changedFragments.hidden.length ) { 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 ) { 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(); this.Reveal.updateControls();

View file

@ -65,11 +65,14 @@ export default class Overview {
const indices = this.Reveal.getIndices(); const indices = this.Reveal.getIndices();
// Notify observers of the overview showing // Notify observers of the overview showing
this.Reveal.dispatchEvent( 'overviewshown', { this.Reveal.dispatchEvent({
type: 'overviewshown',
data: {
'indexh': indices.h, 'indexh': indices.h,
'indexv': indices.v, 'indexv': indices.v,
'currentSlide': this.Reveal.getCurrentSlide() 'currentSlide': this.Reveal.getCurrentSlide()
} ); }
});
} }
@ -175,11 +178,14 @@ export default class Overview {
this.Reveal.cueAutoSlide(); this.Reveal.cueAutoSlide();
// Notify observers of the overview hiding // Notify observers of the overview hiding
this.Reveal.dispatchEvent( 'overviewhidden', { this.Reveal.dispatchEvent({
type: 'overviewhidden',
data: {
'indexh': indices.h, 'indexh': indices.h,
'indexv': indices.v, 'indexv': indices.v,
'currentSlide': this.Reveal.getCurrentSlide() 'currentSlide': this.Reveal.getCurrentSlide()
} ); }
});
} }
} }

View file

@ -194,11 +194,14 @@ export default function( revealElement, options ) {
dom.wrapper.classList.add( 'ready' ); dom.wrapper.classList.add( 'ready' );
dispatchEvent( 'ready', { dispatchEvent({
'indexh': indexh, type: 'ready',
'indexv': indexv, data: {
'currentSlide': currentSlide indexh,
} ); indexv,
currentSlide
}
});
}, 1 ); }, 1 );
// Special setup and config is required when printing to PDF // 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 // 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 * Dispatches an event of the specified type from the
* reveal DOM element. * reveal DOM element.
*/ */
function dispatchEvent( type, args ) { function dispatchEvent({ target=dom.wrapper, type, data, bubbles=true }) {
let event = document.createEvent( 'HTMLEvents', 1, 2 ); let event = document.createEvent( 'HTMLEvents', 1, 2 );
event.initEvent( type, true, true ); event.initEvent( type, bubbles, true );
extend( event, args ); extend( event, data );
dom.wrapper.dispatchEvent( event ); target.dispatchEvent( event );
if( target === dom.wrapper ) {
// If we're in an iframe, post each reveal.js event to the // If we're in an iframe, post each reveal.js event to the
// parent window. Used by the notes plugin // parent window. Used by the notes plugin
dispatchPostMessage( type ); dispatchPostMessage( type );
}
} }
@ -1347,11 +1352,14 @@ export default function( revealElement, options ) {
} }
if( oldScale !== scale ) { if( oldScale !== scale ) {
dispatchEvent( 'resize', { dispatchEvent({
'oldScale': oldScale, type: 'resize',
'scale': scale, data: {
'size': size oldScale,
} ); scale,
size
}
});
} }
} }
@ -1577,7 +1585,7 @@ export default function( revealElement, options ) {
dom.wrapper.classList.add( 'paused' ); dom.wrapper.classList.add( 'paused' );
if( wasPaused === false ) { if( wasPaused === false ) {
dispatchEvent( 'paused' ); dispatchEvent({ type: 'paused' });
} }
} }
@ -1594,7 +1602,7 @@ export default function( revealElement, options ) {
cueAutoSlide(); cueAutoSlide();
if( wasPaused ) { if( wasPaused ) {
dispatchEvent( 'resumed' ); dispatchEvent({ type: 'resumed' });
} }
} }
@ -1763,7 +1771,7 @@ export default function( revealElement, options ) {
document.documentElement.classList.add( state[i] ); document.documentElement.classList.add( state[i] );
// Dispatch custom event matching the state's name // Dispatch custom event matching the state's name
dispatchEvent( state[i] ); dispatchEvent({ type: state[i] });
} }
// Clean up the remains of the previous state // Clean up the remains of the previous state
@ -1772,13 +1780,16 @@ export default function( revealElement, options ) {
} }
if( slideChanged ) { if( slideChanged ) {
dispatchEvent( 'slidechanged', { dispatchEvent({
'indexh': indexh, type: 'slidechanged',
'indexv': indexv, data: {
'previousSlide': previousSlide, indexh,
'currentSlide': currentSlide, indexv,
'origin': o previousSlide,
} ); currentSlide,
origin: o
}
});
} }
// Handle embedded content // 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 // Mark the current slide as present
slides[index].classList.add( 'present' ); slide.classList.add( 'present' );
slides[index].removeAttribute( 'hidden' ); slide.removeAttribute( 'hidden' );
slides[index].removeAttribute( 'aria-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 // If this slide has a state associated with it, add it
// onto the current state of the deck // onto the current state of the deck
let slideState = slides[index].getAttribute( 'data-state' ); let slideState = slide.getAttribute( 'data-state' );
if( slideState ) { if( slideState ) {
state = state.concat( slideState.split( ' ' ) ); state = state.concat( slideState.split( ' ' ) );
} }
@ -2947,7 +2970,7 @@ export default function( revealElement, options ) {
if( autoSlide && !autoSlidePaused ) { if( autoSlide && !autoSlidePaused ) {
autoSlidePaused = true; autoSlidePaused = true;
dispatchEvent( 'autoslidepaused' ); dispatchEvent({ type: 'autoslidepaused' });
clearTimeout( autoSlideTimeout ); clearTimeout( autoSlideTimeout );
if( autoSlidePlayer ) { if( autoSlidePlayer ) {
@ -2961,7 +2984,7 @@ export default function( revealElement, options ) {
if( autoSlide && autoSlidePaused ) { if( autoSlide && autoSlidePaused ) {
autoSlidePaused = false; autoSlidePaused = false;
dispatchEvent( 'autoslideresumed' ); dispatchEvent({ type: 'autoslideresumed' });
cueAutoSlide(); cueAutoSlide();
} }

View file

@ -100,6 +100,15 @@
if( config.highlightOnLoad ) { if( config.highlightOnLoad ) {
RevealHighlight.highlightBlock( block ); 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' ) ) { if( block.hasAttribute( 'data-line-numbers' ) ) {
hljs.lineNumbersBlock( block, { singleLine: true } ); hljs.lineNumbersBlock( block, { singleLine: true } );
var scrollState = { currentBlock: block };
// If there is at least one highlight step, generate // If there is at least one highlight step, generate
// fragments // fragments
var highlightSteps = RevealHighlight.deserializeHighlightSteps( block.getAttribute( 'data-line-numbers' ) ); var highlightSteps = RevealHighlight.deserializeHighlightSteps( block.getAttribute( 'data-line-numbers' ) );
@ -130,6 +141,7 @@
// If the original code block has a fragment-index, // If the original code block has a fragment-index,
// each clone should follow in an incremental sequence // each clone should follow in an incremental sequence
var fragmentIndex = parseInt( block.getAttribute( 'data-fragment-index' ), 10 ); var fragmentIndex = parseInt( block.getAttribute( 'data-fragment-index' ), 10 );
if( typeof fragmentIndex !== 'number' || isNaN( fragmentIndex ) ) { if( typeof fragmentIndex !== 'number' || isNaN( fragmentIndex ) ) {
fragmentIndex = null; fragmentIndex = null;
} }
@ -151,6 +163,10 @@
fragmentBlock.removeAttribute( 'data-fragment-index' ); 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' ) 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 ); 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. * Visually emphasize specific lines within a code block.
* This only works on blocks with line numbering turned on. * This only works on blocks with line numbering turned on.