mirror of
https://github.com/hakimel/reveal.js.git
synced 2025-08-14 18:44:40 +02:00
reader mode progress bar
This commit is contained in:
@@ -10,6 +10,8 @@
|
|||||||
|
|
||||||
@import 'layout';
|
@import 'layout';
|
||||||
|
|
||||||
|
$controlsSpacing: 12px;
|
||||||
|
|
||||||
/*********************************************
|
/*********************************************
|
||||||
* GLOBAL STYLES
|
* GLOBAL STYLES
|
||||||
*********************************************/
|
*********************************************/
|
||||||
@@ -271,13 +273,11 @@ $controlsArrowAngleActive: 36deg;
|
|||||||
}
|
}
|
||||||
|
|
||||||
.reveal .controls {
|
.reveal .controls {
|
||||||
$spacing: 12px;
|
|
||||||
|
|
||||||
display: none;
|
display: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: auto;
|
top: auto;
|
||||||
bottom: $spacing;
|
bottom: $controlsSpacing;
|
||||||
right: $spacing;
|
right: $controlsSpacing;
|
||||||
left: auto;
|
left: auto;
|
||||||
z-index: 11;
|
z-index: 11;
|
||||||
color: #000;
|
color: #000;
|
||||||
@@ -509,7 +509,7 @@ $controlsArrowAngleActive: 36deg;
|
|||||||
// Edge aligned controls layout
|
// Edge aligned controls layout
|
||||||
@media screen and (min-width: 500px) {
|
@media screen and (min-width: 500px) {
|
||||||
|
|
||||||
$spacing: 0.8em;
|
$controlsSpacing: 0.8em;
|
||||||
|
|
||||||
.reveal .controls[data-controls-layout="edges"] {
|
.reveal .controls[data-controls-layout="edges"] {
|
||||||
& {
|
& {
|
||||||
@@ -529,24 +529,24 @@ $controlsArrowAngleActive: 36deg;
|
|||||||
|
|
||||||
.navigate-left {
|
.navigate-left {
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: $spacing;
|
left: $controlsSpacing;
|
||||||
margin-top: -$controlArrowSize*0.5;
|
margin-top: -$controlArrowSize*0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navigate-right {
|
.navigate-right {
|
||||||
top: 50%;
|
top: 50%;
|
||||||
right: $spacing;
|
right: $controlsSpacing;
|
||||||
margin-top: -$controlArrowSize*0.5;
|
margin-top: -$controlArrowSize*0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navigate-up {
|
.navigate-up {
|
||||||
top: $spacing;
|
top: $controlsSpacing;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
margin-left: -$controlArrowSize*0.5;
|
margin-left: -$controlArrowSize*0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navigate-down {
|
.navigate-down {
|
||||||
bottom: $spacing - $controlArrowSpacing + 0.3em;
|
bottom: $controlsSpacing - $controlArrowSpacing + 0.3em;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
margin-left: -$controlArrowSize*0.5;
|
margin-left: -$controlArrowSize*0.5;
|
||||||
}
|
}
|
||||||
@@ -2003,6 +2003,59 @@ $notesWidthPercent: 25%;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.reveal-viewport.reveal-reader::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reveal-viewport.reveal-reader .reader-progress {
|
||||||
|
position: sticky;
|
||||||
|
top: 50%;
|
||||||
|
z-index: 20;
|
||||||
|
|
||||||
|
.reader-progress-inner {
|
||||||
|
position: absolute;
|
||||||
|
width: 8px;
|
||||||
|
height: 90vh;
|
||||||
|
right: $controlsSpacing;
|
||||||
|
top: 0;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
border-radius: 8px;
|
||||||
|
z-index: 10;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-progress-playhead {
|
||||||
|
position: absolute;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: rgba( 255, 255, 255, 0.7);
|
||||||
|
transition: all 0.1s ease;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-progress-slide {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
background-color: rgba( 0, 0, 0, 0.4 );
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-progress-slide:last-child {
|
||||||
|
border-bottom-left-radius: 8px;
|
||||||
|
border-bottom-right-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.reader-progress-slide.active {
|
||||||
|
background-color: #000;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*********************************************
|
/*********************************************
|
||||||
* PRINT STYLES
|
* PRINT STYLES
|
||||||
|
@@ -32,15 +32,14 @@ export default class Reader {
|
|||||||
|
|
||||||
this.slideHTMLBeforeActivation = this.Reveal.getSlidesElement().innerHTML;
|
this.slideHTMLBeforeActivation = this.Reveal.getSlidesElement().innerHTML;
|
||||||
|
|
||||||
const viewportElement = this.Reveal.getViewportElement();
|
|
||||||
const horizontalSlides = queryAll( this.Reveal.getRevealElement(), HORIZONTAL_SLIDES_SELECTOR );
|
const horizontalSlides = queryAll( this.Reveal.getRevealElement(), HORIZONTAL_SLIDES_SELECTOR );
|
||||||
|
|
||||||
viewportElement.classList.add( 'loading-scroll-mode', 'reveal-reader' );
|
this.viewportElement.classList.add( 'loading-scroll-mode', 'reveal-reader' );
|
||||||
viewportElement.addEventListener( 'scroll', this.onScroll );
|
this.viewportElement.addEventListener( 'scroll', this.onScroll );
|
||||||
|
|
||||||
let presentationBackground;
|
let presentationBackground;
|
||||||
|
|
||||||
const viewportStyles = window.getComputedStyle( viewportElement );
|
const viewportStyles = window.getComputedStyle( this.viewportElement );
|
||||||
if( viewportStyles && viewportStyles.background ) {
|
if( viewportStyles && viewportStyles.background ) {
|
||||||
presentationBackground = viewportStyles.background;
|
presentationBackground = viewportStyles.background;
|
||||||
}
|
}
|
||||||
@@ -97,6 +96,8 @@ export default class Reader {
|
|||||||
|
|
||||||
}, this );
|
}, this );
|
||||||
|
|
||||||
|
this.createProgressBar();
|
||||||
|
|
||||||
// Remove leftover stacks
|
// Remove leftover stacks
|
||||||
queryAll( this.Reveal.getRevealElement(), '.stack' ).forEach( stack => stack.remove() );
|
queryAll( this.Reveal.getRevealElement(), '.stack' ).forEach( stack => stack.remove() );
|
||||||
|
|
||||||
@@ -108,13 +109,30 @@ export default class Reader {
|
|||||||
this.Reveal.layout();
|
this.Reveal.layout();
|
||||||
this.Reveal.setState( state );
|
this.Reveal.setState( state );
|
||||||
|
|
||||||
viewportElement.classList.remove( 'loading-scroll-mode' );
|
this.viewportElement.classList.remove( 'loading-scroll-mode' );
|
||||||
|
|
||||||
this.activatedCallbacks.forEach( callback => callback() );
|
this.activatedCallbacks.forEach( callback => callback() );
|
||||||
this.activatedCallbacks = [];
|
this.activatedCallbacks = [];
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createProgressBar() {
|
||||||
|
|
||||||
|
this.progressBar = document.createElement( 'div' );
|
||||||
|
this.progressBar.className = 'reader-progress';
|
||||||
|
|
||||||
|
this.progressBarInner = document.createElement( 'div' );
|
||||||
|
this.progressBarInner.className = 'reader-progress-inner';
|
||||||
|
this.progressBar.appendChild( this.progressBarInner );
|
||||||
|
|
||||||
|
this.progressBarPlayhead = document.createElement( 'div' );
|
||||||
|
this.progressBarPlayhead.className = 'reader-progress-playhead';
|
||||||
|
this.progressBarInner.appendChild( this.progressBarPlayhead );
|
||||||
|
|
||||||
|
this.viewportElement.insertBefore( this.progressBar, this.viewportElement.firstChild );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deactivates the reader mode and restores the standard slide-based
|
* Deactivates the reader mode and restores the standard slide-based
|
||||||
* presentation.
|
* presentation.
|
||||||
@@ -127,10 +145,10 @@ export default class Reader {
|
|||||||
|
|
||||||
this.active = false;
|
this.active = false;
|
||||||
|
|
||||||
const viewportElement = this.Reveal.getViewportElement();
|
this.viewportElement.removeEventListener( 'scroll', this.onScroll );
|
||||||
|
this.viewportElement.classList.remove( 'reveal-reader' );
|
||||||
|
|
||||||
viewportElement.removeEventListener( 'scroll', this.onScroll );
|
this.progressBar.remove();
|
||||||
viewportElement.classList.remove( 'reveal-reader' );
|
|
||||||
|
|
||||||
this.Reveal.getSlidesElement().innerHTML = this.slideHTMLBeforeActivation;
|
this.Reveal.getSlidesElement().innerHTML = this.slideHTMLBeforeActivation;
|
||||||
this.Reveal.sync();
|
this.Reveal.sync();
|
||||||
@@ -159,6 +177,14 @@ export default class Reader {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve a slide by its original h/v index (i.e. the indices the
|
||||||
|
* slide had before being linearized).
|
||||||
|
*
|
||||||
|
* @param {number} h
|
||||||
|
* @param {number} v
|
||||||
|
* @returns {HTMLElement}
|
||||||
|
*/
|
||||||
getSlideByIndices( h, v ) {
|
getSlideByIndices( h, v ) {
|
||||||
|
|
||||||
const page = this.pages.find( page => page.indexh === h && page.indexv === v );
|
const page = this.pages.find( page => page.indexh === h && page.indexv === v );
|
||||||
@@ -179,16 +205,15 @@ export default class Reader {
|
|||||||
const scale = this.Reveal.getScale();
|
const scale = this.Reveal.getScale();
|
||||||
const readerLayout = config.readerLayout;
|
const readerLayout = config.readerLayout;
|
||||||
|
|
||||||
const viewportElement = this.Reveal.getViewportElement();
|
const viewportHeight = this.viewportElement.offsetHeight;
|
||||||
const viewportHeight = viewportElement.offsetHeight;
|
|
||||||
const compactHeight = slideSize.height * scale;
|
const compactHeight = slideSize.height * scale;
|
||||||
const pageHeight = readerLayout === 'full' ? viewportHeight : compactHeight;
|
const pageHeight = readerLayout === 'full' ? viewportHeight : compactHeight;
|
||||||
|
|
||||||
// The height that needs to be scrolled between scroll triggers
|
// The height that needs to be scrolled between scroll triggers
|
||||||
const scrollTriggerHeight = viewportHeight / 2;
|
const scrollTriggerHeight = viewportHeight / 2;
|
||||||
|
|
||||||
viewportElement.style.setProperty( '--page-height', pageHeight + 'px' );
|
this.viewportElement.style.setProperty( '--page-height', pageHeight + 'px' );
|
||||||
viewportElement.style.scrollSnapType = typeof config.readerScrollSnap === 'string' ?
|
this.viewportElement.style.scrollSnapType = typeof config.readerScrollSnap === 'string' ?
|
||||||
`y ${config.readerScrollSnap}` : '';
|
`y ${config.readerScrollSnap}` : '';
|
||||||
|
|
||||||
const pageElements = Array.from( this.Reveal.getRevealElement().querySelectorAll( '.reader-page' ) );
|
const pageElements = Array.from( this.Reveal.getRevealElement().querySelectorAll( '.reader-page' ) );
|
||||||
@@ -270,12 +295,56 @@ export default class Reader {
|
|||||||
return page;
|
return page;
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
this.createProgressBarSlides();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
createProgressBarSlides() {
|
||||||
|
|
||||||
|
this.progressBarInner.querySelectorAll( '.reader-progress-slide' ).forEach( slide => slide.remove() );
|
||||||
|
|
||||||
|
const spacing = 2;
|
||||||
|
|
||||||
|
const viewportHeight = this.viewportElement.offsetHeight;
|
||||||
|
const scrollHeight = this.viewportElement.scrollHeight;
|
||||||
|
|
||||||
|
this.progressBarHeight = this.progressBarInner.offsetHeight;
|
||||||
|
this.playheadHeight = viewportHeight / scrollHeight * this.progressBarHeight;
|
||||||
|
this.progressBarScrollableHeight = this.progressBarHeight - this.playheadHeight;
|
||||||
|
|
||||||
|
this.progressBarPlayhead.style.height = this.playheadHeight - spacing + 'px';
|
||||||
|
|
||||||
|
this.pages.forEach( page => {
|
||||||
|
|
||||||
|
page.progressBarSlide = document.createElement( 'div' );
|
||||||
|
page.progressBarSlide.className = 'reader-progress-slide';
|
||||||
|
page.progressBarSlide.style.top = page.top / scrollHeight * this.progressBarHeight + 'px';
|
||||||
|
page.progressBarSlide.style.height = page.totalHeight / scrollHeight * this.progressBarHeight - spacing + 'px';
|
||||||
|
this.progressBarInner.appendChild( page.progressBarSlide );
|
||||||
|
|
||||||
|
} );
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
layout() {
|
layout() {
|
||||||
|
|
||||||
|
if( this.isActive() ) {
|
||||||
this.sync();
|
this.sync();
|
||||||
this.onScroll();
|
this.onScroll();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
moveProgressBarTo( progress ) {
|
||||||
|
|
||||||
|
this.progressBarPlayhead.style.transform = `translateY(${progress * this.progressBarScrollableHeight}px)`;
|
||||||
|
|
||||||
|
this.pages.forEach( ( page ) => {
|
||||||
|
page.progressBarSlide.classList.toggle( 'active', !!page.active );
|
||||||
|
page.scrollTriggers.forEach( trigger => {
|
||||||
|
// page.progressBarSlide.classList.toggle( 'active', !!trigger.active );
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -292,10 +361,8 @@ export default class Reader {
|
|||||||
|
|
||||||
onScroll() {
|
onScroll() {
|
||||||
|
|
||||||
const viewportElement = this.Reveal.getViewportElement();
|
const viewportHeight = this.viewportElement.offsetHeight;
|
||||||
const viewportHeight = viewportElement.offsetHeight;
|
const scrollTop = this.viewportElement.scrollTop;
|
||||||
|
|
||||||
const scrollTop = viewportElement.scrollTop;
|
|
||||||
|
|
||||||
// Find the page closest to the center of the viewport, this
|
// Find the page closest to the center of the viewport, this
|
||||||
// is the page we want to focus and activate
|
// is the page we want to focus and activate
|
||||||
@@ -367,6 +434,14 @@ export default class Reader {
|
|||||||
}
|
}
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
this.moveProgressBarTo( scrollTop / ( this.viewportElement.scrollHeight - viewportHeight ) );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
get viewportElement() {
|
||||||
|
|
||||||
|
return this.Reveal.getViewportElement();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user