ソースを参照

reader mode progress bar

Hakim El Hattab 1 年間 前
コミット
f80ee3b917
2 ファイル変更155 行追加27 行削除
  1. 62 9
      css/reveal.scss
  2. 93 18
      js/controllers/reader.js

+ 62 - 9
css/reveal.scss

@@ -10,6 +10,8 @@
 
 @import 'layout';
 
+$controlsSpacing: 12px;
+
 /*********************************************
  * GLOBAL STYLES
  *********************************************/
@@ -271,13 +273,11 @@ $controlsArrowAngleActive: 36deg;
 }
 
 .reveal .controls {
-	$spacing: 12px;
-
 	display: none;
 	position: absolute;
 	top: auto;
-	bottom: $spacing;
-	right: $spacing;
+	bottom: $controlsSpacing;
+	right: $controlsSpacing;
 	left: auto;
 	z-index: 11;
 	color: #000;
@@ -509,7 +509,7 @@ $controlsArrowAngleActive: 36deg;
 // Edge aligned controls layout
 @media screen and (min-width: 500px) {
 
-	$spacing: 0.8em;
+	$controlsSpacing: 0.8em;
 
 	.reveal .controls[data-controls-layout="edges"] {
 		& {
@@ -529,24 +529,24 @@ $controlsArrowAngleActive: 36deg;
 
 		.navigate-left {
 			top: 50%;
-			left: $spacing;
+			left: $controlsSpacing;
 			margin-top: -$controlArrowSize*0.5;
 		}
 
 		.navigate-right {
 			top: 50%;
-			right: $spacing;
+			right: $controlsSpacing;
 			margin-top: -$controlArrowSize*0.5;
 		}
 
 		.navigate-up {
-			top: $spacing;
+			top: $controlsSpacing;
 			left: 50%;
 			margin-left: -$controlArrowSize*0.5;
 		}
 
 		.navigate-down {
-			bottom: $spacing - $controlArrowSpacing + 0.3em;
+			bottom: $controlsSpacing - $controlArrowSpacing + 0.3em;
 			left: 50%;
 			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

+ 93 - 18
js/controllers/reader.js

@@ -32,15 +32,14 @@ export default class Reader {
 
 		this.slideHTMLBeforeActivation = this.Reveal.getSlidesElement().innerHTML;
 
-		const viewportElement = this.Reveal.getViewportElement();
 		const horizontalSlides = queryAll( this.Reveal.getRevealElement(), HORIZONTAL_SLIDES_SELECTOR );
 
-		viewportElement.classList.add( 'loading-scroll-mode', 'reveal-reader' );
-		viewportElement.addEventListener( 'scroll', this.onScroll );
+		this.viewportElement.classList.add( 'loading-scroll-mode', 'reveal-reader' );
+		this.viewportElement.addEventListener( 'scroll', this.onScroll );
 
 		let presentationBackground;
 
-		const viewportStyles = window.getComputedStyle( viewportElement );
+		const viewportStyles = window.getComputedStyle( this.viewportElement );
 		if( viewportStyles && viewportStyles.background ) {
 			presentationBackground = viewportStyles.background;
 		}
@@ -97,6 +96,8 @@ export default class Reader {
 
 		}, this );
 
+		this.createProgressBar();
+
 		// Remove leftover stacks
 		queryAll( this.Reveal.getRevealElement(), '.stack' ).forEach( stack => stack.remove() );
 
@@ -108,13 +109,30 @@ export default class Reader {
 		this.Reveal.layout();
 		this.Reveal.setState( state );
 
-		viewportElement.classList.remove( 'loading-scroll-mode' );
+		this.viewportElement.classList.remove( 'loading-scroll-mode' );
 
 		this.activatedCallbacks.forEach( callback => callback() );
 		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
 	 * presentation.
@@ -127,10 +145,10 @@ export default class Reader {
 
 		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 );
-		viewportElement.classList.remove( 'reveal-reader' );
+		this.progressBar.remove();
 
 		this.Reveal.getSlidesElement().innerHTML = this.slideHTMLBeforeActivation;
 		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 ) {
 
 		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 readerLayout = config.readerLayout;
 
-		const viewportElement = this.Reveal.getViewportElement();
-		const viewportHeight = viewportElement.offsetHeight;
+		const viewportHeight = this.viewportElement.offsetHeight;
 		const compactHeight = slideSize.height * scale;
 		const pageHeight = readerLayout === 'full' ? viewportHeight : compactHeight;
 
 		// The height that needs to be scrolled between scroll triggers
 		const scrollTriggerHeight = viewportHeight / 2;
 
-		viewportElement.style.setProperty( '--page-height', pageHeight + 'px' );
-		viewportElement.style.scrollSnapType = typeof config.readerScrollSnap === 'string' ?
+		this.viewportElement.style.setProperty( '--page-height', pageHeight + 'px' );
+		this.viewportElement.style.scrollSnapType = typeof config.readerScrollSnap === 'string' ?
 												`y ${config.readerScrollSnap}` : '';
 
 		const pageElements = Array.from( this.Reveal.getRevealElement().querySelectorAll( '.reader-page' ) );
@@ -270,12 +295,56 @@ export default class Reader {
 			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() {
 
-		this.sync();
-		this.onScroll();
+		if( this.isActive() ) {
+			this.sync();
+			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() {
 
-		const viewportElement = this.Reveal.getViewportElement();
-		const viewportHeight = viewportElement.offsetHeight;
-
-		const scrollTop = viewportElement.scrollTop;
+		const viewportHeight = this.viewportElement.offsetHeight;
+		const scrollTop = this.viewportElement.scrollTop;
 
 		// Find the page closest to the center of the viewport, this
 		// 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();
+
 	}
 
 }