blob: 1181bb53e2e6e209211e48fe5a03aed72268fa87 [file] [log] [blame]
Marc Kupietz9c036a42024-05-14 13:17:25 +02001import { HORIZONTAL_SLIDES_SELECTOR, HORIZONTAL_BACKGROUNDS_SELECTOR } from '../utils/constants.js'
2import { queryAll } from '../utils/util.js'
3
4const HIDE_SCROLLBAR_TIMEOUT = 500;
5const MAX_PROGRESS_SPACING = 4;
6const MIN_PROGRESS_SEGMENT_HEIGHT = 6;
7const MIN_PLAYHEAD_HEIGHT = 8;
8
9/**
10 * The scroll view lets you read a reveal.js presentation
11 * as a linear scrollable page.
12 */
13export default class ScrollView {
14
15 constructor( Reveal ) {
16
17 this.Reveal = Reveal;
18
19 this.active = false;
20 this.activatedCallbacks = [];
21
22 this.onScroll = this.onScroll.bind( this );
23
24 }
25
26 /**
27 * Activates the scroll view. This rearranges the presentation DOM
28 * by—among other things—wrapping each slide in a page element.
29 */
30 activate() {
31
32 if( this.active ) return;
33
34 const stateBeforeActivation = this.Reveal.getState();
35
36 this.active = true;
37
38 // Store the full presentation HTML so that we can restore it
39 // when/if the scroll view is deactivated
40 this.slideHTMLBeforeActivation = this.Reveal.getSlidesElement().innerHTML;
41
42 const horizontalSlides = queryAll( this.Reveal.getRevealElement(), HORIZONTAL_SLIDES_SELECTOR );
43 const horizontalBackgrounds = queryAll( this.Reveal.getRevealElement(), HORIZONTAL_BACKGROUNDS_SELECTOR );
44
45 this.viewportElement.classList.add( 'loading-scroll-mode', 'reveal-scroll' );
46
47 let presentationBackground;
48
49 const viewportStyles = window.getComputedStyle( this.viewportElement );
50 if( viewportStyles && viewportStyles.background ) {
51 presentationBackground = viewportStyles.background;
52 }
53
54 const pageElements = [];
55 const pageContainer = horizontalSlides[0].parentNode;
56
57 let previousSlide;
58
59 // Creates a new page element and appends the given slide/bg
60 // to it.
61 const createPageElement = ( slide, h, v, isVertical ) => {
62
63 let contentContainer;
64
65 // If this slide is part of an auto-animation sequence, we
66 // group it under the same page element as the previous slide
67 if( previousSlide && this.Reveal.shouldAutoAnimateBetween( previousSlide, slide ) ) {
68 contentContainer = document.createElement( 'div' );
69 contentContainer.className = 'scroll-page-content scroll-auto-animate-page';
70 contentContainer.style.display = 'none';
71 previousSlide.closest( '.scroll-page-content' ).parentNode.appendChild( contentContainer );
72 }
73 else {
74 // Wrap the slide in a page element and hide its overflow
75 // so that no page ever flows onto another
76 const page = document.createElement( 'div' );
77 page.className = 'scroll-page';
78 pageElements.push( page );
79
80 // This transfers over the background of the vertical stack containing
81 // the slide if it exists. Otherwise, it uses the presentation-wide
82 // background.
83 if( isVertical && horizontalBackgrounds.length > h ) {
84 const slideBackground = horizontalBackgrounds[h];
85 const pageBackground = window.getComputedStyle( slideBackground );
86
87 if( pageBackground && pageBackground.background ) {
88 page.style.background = pageBackground.background;
89 }
90 else if( presentationBackground ) {
91 page.style.background = presentationBackground;
92 }
93 } else if( presentationBackground ) {
94 page.style.background = presentationBackground;
95 }
96
97 const stickyContainer = document.createElement( 'div' );
98 stickyContainer.className = 'scroll-page-sticky';
99 page.appendChild( stickyContainer );
100
101 contentContainer = document.createElement( 'div' );
102 contentContainer.className = 'scroll-page-content';
103 stickyContainer.appendChild( contentContainer );
104 }
105
106 contentContainer.appendChild( slide );
107
108 slide.classList.remove( 'past', 'future' );
109 slide.setAttribute( 'data-index-h', h );
110 slide.setAttribute( 'data-index-v', v );
111
112 if( slide.slideBackgroundElement ) {
113 slide.slideBackgroundElement.remove( 'past', 'future' );
114 contentContainer.insertBefore( slide.slideBackgroundElement, slide );
115 }
116
117 previousSlide = slide;
118
119 }
120
121 // Slide and slide background layout
122 horizontalSlides.forEach( ( horizontalSlide, h ) => {
123
124 if( this.Reveal.isVerticalStack( horizontalSlide ) ) {
125 horizontalSlide.querySelectorAll( 'section' ).forEach( ( verticalSlide, v ) => {
126 createPageElement( verticalSlide, h, v, true );
127 });
128 }
129 else {
130 createPageElement( horizontalSlide, h, 0 );
131 }
132
133 }, this );
134
135 this.createProgressBar();
136
137 // Remove leftover stacks
138 queryAll( this.Reveal.getRevealElement(), '.stack' ).forEach( stack => stack.remove() );
139
140 // Add our newly created pages to the DOM
141 pageElements.forEach( page => pageContainer.appendChild( page ) );
142
143 // Re-run JS-based content layout after the slide is added to page DOM
144 this.Reveal.slideContent.layout( this.Reveal.getSlidesElement() );
145
146 this.Reveal.layout();
147 this.Reveal.setState( stateBeforeActivation );
148
149 this.activatedCallbacks.forEach( callback => callback() );
150 this.activatedCallbacks = [];
151
152 this.restoreScrollPosition();
153
154 this.viewportElement.classList.remove( 'loading-scroll-mode' );
155 this.viewportElement.addEventListener( 'scroll', this.onScroll, { passive: true } );
156
157 }
158
159 /**
160 * Deactivates the scroll view and restores the standard slide-based
161 * presentation.
162 */
163 deactivate() {
164
165 if( !this.active ) return;
166
167 const stateBeforeDeactivation = this.Reveal.getState();
168
169 this.active = false;
170
171 this.viewportElement.removeEventListener( 'scroll', this.onScroll );
172 this.viewportElement.classList.remove( 'reveal-scroll' );
173
174 this.removeProgressBar();
175
176 this.Reveal.getSlidesElement().innerHTML = this.slideHTMLBeforeActivation;
177 this.Reveal.sync();
178 this.Reveal.setState( stateBeforeDeactivation );
179
180 this.slideHTMLBeforeActivation = null;
181
182 }
183
184 toggle( override ) {
185
186 if( typeof override === 'boolean' ) {
187 override ? this.activate() : this.deactivate();
188 }
189 else {
190 this.isActive() ? this.deactivate() : this.activate();
191 }
192
193 }
194
195 /**
196 * Checks if the scroll view is currently active.
197 */
198 isActive() {
199
200 return this.active;
201
202 }
203
204 /**
205 * Renders the progress bar component.
206 */
207 createProgressBar() {
208
209 this.progressBar = document.createElement( 'div' );
210 this.progressBar.className = 'scrollbar';
211
212 this.progressBarInner = document.createElement( 'div' );
213 this.progressBarInner.className = 'scrollbar-inner';
214 this.progressBar.appendChild( this.progressBarInner );
215
216 this.progressBarPlayhead = document.createElement( 'div' );
217 this.progressBarPlayhead.className = 'scrollbar-playhead';
218 this.progressBarInner.appendChild( this.progressBarPlayhead );
219
220 this.viewportElement.insertBefore( this.progressBar, this.viewportElement.firstChild );
221
222 const handleDocumentMouseMove = ( event ) => {
223
224 let progress = ( event.clientY - this.progressBarInner.getBoundingClientRect().top ) / this.progressBarHeight;
225 progress = Math.max( Math.min( progress, 1 ), 0 );
226
227 this.viewportElement.scrollTop = progress * ( this.viewportElement.scrollHeight - this.viewportElement.offsetHeight );
228
229 };
230
231 const handleDocumentMouseUp = ( event ) => {
232
233 this.draggingProgressBar = false;
234 this.showProgressBar();
235
236 document.removeEventListener( 'mousemove', handleDocumentMouseMove );
237 document.removeEventListener( 'mouseup', handleDocumentMouseUp );
238
239 };
240
241 const handleMouseDown = ( event ) => {
242
243 event.preventDefault();
244
245 this.draggingProgressBar = true;
246
247 document.addEventListener( 'mousemove', handleDocumentMouseMove );
248 document.addEventListener( 'mouseup', handleDocumentMouseUp );
249
250 handleDocumentMouseMove( event );
251
252 };
253
254 this.progressBarInner.addEventListener( 'mousedown', handleMouseDown );
255
256 }
257
258 removeProgressBar() {
259
260 if( this.progressBar ) {
261 this.progressBar.remove();
262 this.progressBar = null;
263 }
264
265 }
266
267 layout() {
268
269 if( this.isActive() ) {
270 this.syncPages();
271 this.syncScrollPosition();
272 }
273
274 }
275
276 /**
277 * Updates our pages to match the latest configuration and
278 * presentation size.
279 */
280 syncPages() {
281
282 const config = this.Reveal.getConfig();
283
284 const slideSize = this.Reveal.getComputedSlideSize( window.innerWidth, window.innerHeight );
285 const scale = this.Reveal.getScale();
286 const useCompactLayout = config.scrollLayout === 'compact';
287
288 const viewportHeight = this.viewportElement.offsetHeight;
289 const compactHeight = slideSize.height * scale;
290 const pageHeight = useCompactLayout ? compactHeight : viewportHeight;
291
292 // The height that needs to be scrolled between scroll triggers
293 this.scrollTriggerHeight = useCompactLayout ? compactHeight : viewportHeight;
294
295 this.viewportElement.style.setProperty( '--page-height', pageHeight + 'px' );
296 this.viewportElement.style.scrollSnapType = typeof config.scrollSnap === 'string' ? `y ${config.scrollSnap}` : '';
297
298 // This will hold all scroll triggers used to show/hide slides
299 this.slideTriggers = [];
300
301 const pageElements = Array.from( this.Reveal.getRevealElement().querySelectorAll( '.scroll-page' ) );
302
303 this.pages = pageElements.map( pageElement => {
304 const page = this.createPage({
305 pageElement,
306 slideElement: pageElement.querySelector( 'section' ),
307 stickyElement: pageElement.querySelector( '.scroll-page-sticky' ),
308 contentElement: pageElement.querySelector( '.scroll-page-content' ),
309 backgroundElement: pageElement.querySelector( '.slide-background' ),
310 autoAnimateElements: pageElement.querySelectorAll( '.scroll-auto-animate-page' ),
311 autoAnimatePages: []
312 });
313
314 page.pageElement.style.setProperty( '--slide-height', config.center === true ? 'auto' : slideSize.height + 'px' );
315
316 this.slideTriggers.push({
317 page: page,
318 activate: () => this.activatePage( page ),
319 deactivate: () => this.deactivatePage( page )
320 });
321
322 // Create scroll triggers that show/hide fragments
323 this.createFragmentTriggersForPage( page );
324
325 // Create scroll triggers for triggering auto-animate steps
326 if( page.autoAnimateElements.length > 0 ) {
327 this.createAutoAnimateTriggersForPage( page );
328 }
329
330 let totalScrollTriggerCount = Math.max( page.scrollTriggers.length - 1, 0 );
331
332 // Each auto-animate step may include its own scroll triggers
333 // for fragments, ensure we count those as well
334 totalScrollTriggerCount += page.autoAnimatePages.reduce( ( total, page ) => {
335 return total + Math.max( page.scrollTriggers.length - 1, 0 );
336 }, page.autoAnimatePages.length );
337
338 // Clean up from previous renders
339 page.pageElement.querySelectorAll( '.scroll-snap-point' ).forEach( el => el.remove() );
340
341 // Create snap points for all scroll triggers
342 // - Can't be absolute in FF
343 // - Can't be 0-height in Safari
344 // - Can't use snap-align on parent in Safari because then
345 // inner triggers won't work
346 for( let i = 0; i < totalScrollTriggerCount + 1; i++ ) {
347 const triggerStick = document.createElement( 'div' );
348 triggerStick.className = 'scroll-snap-point';
349 triggerStick.style.height = this.scrollTriggerHeight + 'px';
350 triggerStick.style.scrollSnapAlign = useCompactLayout ? 'center' : 'start';
351 page.pageElement.appendChild( triggerStick );
352
353 if( i === 0 ) {
354 triggerStick.style.marginTop = -this.scrollTriggerHeight + 'px';
355 }
356 }
357
358 // In the compact layout, only slides with scroll triggers cover the
359 // full viewport height. This helps avoid empty gaps before or after
360 // a sticky slide.
361 if( useCompactLayout && page.scrollTriggers.length > 0 ) {
362 page.pageHeight = viewportHeight;
363 page.pageElement.style.setProperty( '--page-height', viewportHeight + 'px' );
364 }
365 else {
366 page.pageHeight = pageHeight;
367 page.pageElement.style.removeProperty( '--page-height' );
368 }
369
370 // Add scroll padding based on how many scroll triggers we have
371 page.scrollPadding = this.scrollTriggerHeight * totalScrollTriggerCount;
372
373 // The total height including scrollable space
374 page.totalHeight = page.pageHeight + page.scrollPadding;
375
376 // This is used to pad the height of our page in CSS
377 page.pageElement.style.setProperty( '--page-scroll-padding', page.scrollPadding + 'px' );
378
379 // If this is a sticky page, stick it to the vertical center
380 if( totalScrollTriggerCount > 0 ) {
381 page.stickyElement.style.position = 'sticky';
382 page.stickyElement.style.top = Math.max( ( viewportHeight - page.pageHeight ) / 2, 0 ) + 'px';
383 }
384 else {
385 page.stickyElement.style.position = 'relative';
386 page.pageElement.style.scrollSnapAlign = page.pageHeight < viewportHeight ? 'center' : 'start';
387 }
388
389 return page;
390 } );
391
392 this.setTriggerRanges();
393
394 /*
395 console.log(this.slideTriggers.map( t => {
396 return {
397 range: `${t.range[0].toFixed(2)}-${t.range[1].toFixed(2)}`,
398 triggers: t.page.scrollTriggers.map( t => {
399 return `${t.range[0].toFixed(2)}-${t.range[1].toFixed(2)}`
400 }).join( ', ' ),
401 }
402 }))
403 */
404
405 this.viewportElement.setAttribute( 'data-scrollbar', config.scrollProgress );
406
407 if( config.scrollProgress && this.totalScrollTriggerCount > 1 ) {
408 // Create the progress bar if it doesn't already exist
409 if( !this.progressBar ) this.createProgressBar();
410
411 this.syncProgressBar();
412 }
413 else {
414 this.removeProgressBar();
415 }
416
417 }
418
419 /**
420 * Calculates and sets the scroll range for all of our scroll
421 * triggers.
422 */
423 setTriggerRanges() {
424
425 // Calculate the total number of scroll triggers
426 this.totalScrollTriggerCount = this.slideTriggers.reduce( ( total, trigger ) => {
427 return total + Math.max( trigger.page.scrollTriggers.length, 1 );
428 }, 0 );
429
430 let rangeStart = 0;
431
432 // Calculate the scroll range of each scroll trigger on a scale
433 // of 0-1
434 this.slideTriggers.forEach( ( trigger, i ) => {
435 trigger.range = [
436 rangeStart,
437 rangeStart + Math.max( trigger.page.scrollTriggers.length, 1 ) / this.totalScrollTriggerCount
438 ];
439
440 const scrollTriggerSegmentSize = ( trigger.range[1] - trigger.range[0] ) / trigger.page.scrollTriggers.length;
441 // Set the range for each inner scroll trigger
442 trigger.page.scrollTriggers.forEach( ( scrollTrigger, i ) => {
443 scrollTrigger.range = [
444 rangeStart + i * scrollTriggerSegmentSize,
445 rangeStart + ( i + 1 ) * scrollTriggerSegmentSize
446 ];
447 } );
448
449 rangeStart = trigger.range[1];
450 } );
451
Marc Kupietzcf6e9982026-08-15 15:37:40 +0200452 // Ensure the last trigger extends to the end of the page, otherwise
453 // rounding errors can cause the last trigger to end at 0.999999...
454 this.slideTriggers[this.slideTriggers.length - 1].range[1] = 1;
455
Marc Kupietz9c036a42024-05-14 13:17:25 +0200456 }
457
458 /**
459 * Creates one scroll trigger for each fragments in the given page.
460 *
461 * @param {*} page
462 */
463 createFragmentTriggersForPage( page, slideElement ) {
464
465 slideElement = slideElement || page.slideElement;
466
467 // Each fragment 'group' is an array containing one or more
468 // fragments. Multiple fragments that appear at the same time
469 // are part of the same group.
470 const fragmentGroups = this.Reveal.fragments.sort( slideElement.querySelectorAll( '.fragment' ), true );
471
472 // Create scroll triggers that show/hide fragments
473 if( fragmentGroups.length ) {
474 page.fragments = this.Reveal.fragments.sort( slideElement.querySelectorAll( '.fragment:not(.disabled)' ) );
475 page.scrollTriggers.push(
476 // Trigger for the initial state with no fragments visible
477 {
478 activate: () => {
479 this.Reveal.fragments.update( -1, page.fragments, slideElement );
480 }
481 }
482 );
483
484 // Triggers for each fragment group
485 fragmentGroups.forEach( ( fragments, i ) => {
486 page.scrollTriggers.push({
487 activate: () => {
488 this.Reveal.fragments.update( i, page.fragments, slideElement );
489 }
490 });
491 } );
492 }
493
494
495 return page.scrollTriggers.length;
496
497 }
498
499 /**
500 * Creates scroll triggers for the auto-animate steps in the
501 * given page.
502 *
503 * @param {*} page
504 */
505 createAutoAnimateTriggersForPage( page ) {
506
507 if( page.autoAnimateElements.length > 0 ) {
508
509 // Triggers for each subsequent auto-animate slide
510 this.slideTriggers.push( ...Array.from( page.autoAnimateElements ).map( ( autoAnimateElement, i ) => {
511 let autoAnimatePage = this.createPage({
512 slideElement: autoAnimateElement.querySelector( 'section' ),
513 contentElement: autoAnimateElement,
514 backgroundElement: autoAnimateElement.querySelector( '.slide-background' )
515 });
516
517 // Create fragment scroll triggers for the auto-animate slide
518 this.createFragmentTriggersForPage( autoAnimatePage, autoAnimatePage.slideElement );
519
520 page.autoAnimatePages.push( autoAnimatePage );
521
522 // Return our slide trigger
523 return {
524 page: autoAnimatePage,
525 activate: () => this.activatePage( autoAnimatePage ),
526 deactivate: () => this.deactivatePage( autoAnimatePage )
527 };
528 }));
529 }
530
531 }
532
533 /**
534 * Helper method for creating a page definition and adding
535 * required fields. A "page" is a slide or auto-animate step.
536 */
537 createPage( page ) {
538
539 page.scrollTriggers = [];
540 page.indexh = parseInt( page.slideElement.getAttribute( 'data-index-h' ), 10 );
541 page.indexv = parseInt( page.slideElement.getAttribute( 'data-index-v' ), 10 );
542
543 return page;
544
545 }
546
547 /**
548 * Rerenders progress bar segments so that they match the current
549 * reveal.js config and size.
550 */
551 syncProgressBar() {
552
553 this.progressBarInner.querySelectorAll( '.scrollbar-slide' ).forEach( slide => slide.remove() );
554
555 const scrollHeight = this.viewportElement.scrollHeight;
556 const viewportHeight = this.viewportElement.offsetHeight;
557 const viewportHeightFactor = viewportHeight / scrollHeight;
558
559 this.progressBarHeight = this.progressBarInner.offsetHeight;
560 this.playheadHeight = Math.max( viewportHeightFactor * this.progressBarHeight, MIN_PLAYHEAD_HEIGHT );
561 this.progressBarScrollableHeight = this.progressBarHeight - this.playheadHeight;
562
563 const progressSegmentHeight = viewportHeight / scrollHeight * this.progressBarHeight;
564 const spacing = Math.min( progressSegmentHeight / 8, MAX_PROGRESS_SPACING );
565
566 this.progressBarPlayhead.style.height = this.playheadHeight - spacing + 'px';
567
568 // Don't show individual segments if they're too small
569 if( progressSegmentHeight > MIN_PROGRESS_SEGMENT_HEIGHT ) {
570
571 this.slideTriggers.forEach( slideTrigger => {
572
573 const { page } = slideTrigger;
574
575 // Visual representation of a slide
576 page.progressBarSlide = document.createElement( 'div' );
577 page.progressBarSlide.className = 'scrollbar-slide';
578 page.progressBarSlide.style.top = slideTrigger.range[0] * this.progressBarHeight + 'px';
579 page.progressBarSlide.style.height = ( slideTrigger.range[1] - slideTrigger.range[0] ) * this.progressBarHeight - spacing + 'px';
580 page.progressBarSlide.classList.toggle( 'has-triggers', page.scrollTriggers.length > 0 );
581 this.progressBarInner.appendChild( page.progressBarSlide );
582
583 // Visual representations of each scroll trigger
584 page.scrollTriggerElements = page.scrollTriggers.map( ( trigger, i ) => {
585
586 const triggerElement = document.createElement( 'div' );
587 triggerElement.className = 'scrollbar-trigger';
588 triggerElement.style.top = ( trigger.range[0] - slideTrigger.range[0] ) * this.progressBarHeight + 'px';
589 triggerElement.style.height = ( trigger.range[1] - trigger.range[0] ) * this.progressBarHeight - spacing + 'px';
590 page.progressBarSlide.appendChild( triggerElement );
591
592 if( i === 0 ) triggerElement.style.display = 'none';
593
594 return triggerElement;
595
596 } );
597
598 } );
599
600 }
601 else {
602
603 this.pages.forEach( page => page.progressBarSlide = null );
604
605 }
606
607 }
608
609 /**
610 * Reads the current scroll position and updates our active
611 * trigger states accordingly.
612 */
613 syncScrollPosition() {
614
615 const viewportHeight = this.viewportElement.offsetHeight;
616 const viewportHeightFactor = viewportHeight / this.viewportElement.scrollHeight;
617
618 const scrollTop = this.viewportElement.scrollTop;
619 const scrollHeight = this.viewportElement.scrollHeight - viewportHeight
620 const scrollProgress = Math.max( Math.min( scrollTop / scrollHeight, 1 ), 0 );
621 const scrollProgressMid = Math.max( Math.min( ( scrollTop + viewportHeight / 2 ) / this.viewportElement.scrollHeight, 1 ), 0 );
622
623 let activePage;
624
625 this.slideTriggers.forEach( ( trigger ) => {
626 const { page } = trigger;
627
628 const shouldPreload = scrollProgress >= trigger.range[0] - viewportHeightFactor*2 &&
629 scrollProgress <= trigger.range[1] + viewportHeightFactor*2;
630
631 // Load slides that are within the preload range
632 if( shouldPreload && !page.loaded ) {
633 page.loaded = true;
634 this.Reveal.slideContent.load( page.slideElement );
635 }
636 else if( page.loaded ) {
637 page.loaded = false;
638 this.Reveal.slideContent.unload( page.slideElement );
639 }
640
641 // If we're within this trigger range, activate it
642 if( scrollProgress >= trigger.range[0] && scrollProgress <= trigger.range[1] ) {
643 this.activateTrigger( trigger );
644 activePage = trigger.page;
645 }
646 // .. otherwise deactivate
647 else if( trigger.active ) {
648 this.deactivateTrigger( trigger );
649 }
650 } );
651
652 // Each page can have its own scroll triggers, check if any of those
653 // need to be activated/deactivated
654 if( activePage ) {
655 activePage.scrollTriggers.forEach( ( trigger ) => {
656 if( scrollProgressMid >= trigger.range[0] && scrollProgressMid <= trigger.range[1] ) {
657 this.activateTrigger( trigger );
658 }
659 else if( trigger.active ) {
660 this.deactivateTrigger( trigger );
661 }
662 } );
663 }
664
665 // Update our visual progress indication
666 this.setProgressBarValue( scrollTop / ( this.viewportElement.scrollHeight - viewportHeight ) );
667
668 }
669
670 /**
671 * Moves the progress bar playhead to the specified position.
672 *
673 * @param {number} progress 0-1
674 */
675 setProgressBarValue( progress ) {
676
677 if( this.progressBar ) {
678
679 this.progressBarPlayhead.style.transform = `translateY(${progress * this.progressBarScrollableHeight}px)`;
680
681 this.getAllPages()
682 .filter( page => page.progressBarSlide )
683 .forEach( ( page ) => {
684 page.progressBarSlide.classList.toggle( 'active', page.active === true );
685
686 page.scrollTriggers.forEach( ( trigger, i ) => {
687 page.scrollTriggerElements[i].classList.toggle( 'active', page.active === true && trigger.active === true );
688 } );
689 } );
690
691 this.showProgressBar();
692
693 }
694
695 }
696
697 /**
698 * Show the progress bar and, if configured, automatically hide
699 * it after a delay.
700 */
701 showProgressBar() {
702
703 this.progressBar.classList.add( 'visible' );
704
705 clearTimeout( this.hideProgressBarTimeout );
706
707 if( this.Reveal.getConfig().scrollProgress === 'auto' && !this.draggingProgressBar ) {
708
709 this.hideProgressBarTimeout = setTimeout( () => {
710 if( this.progressBar ) {
711 this.progressBar.classList.remove( 'visible' );
712 }
713 }, HIDE_SCROLLBAR_TIMEOUT );
714
715 }
716
717 }
718
719 /**
720 * Scroll to the previous page.
721 */
722 prev() {
723
724 this.viewportElement.scrollTop -= this.scrollTriggerHeight;
725
726 }
727
728 /**
729 * Scroll to the next page.
730 */
731 next() {
732
733 this.viewportElement.scrollTop += this.scrollTriggerHeight;
734
735 }
736
737 /**
738 * Scrolls the given slide element into view.
739 *
740 * @param {HTMLElement} slideElement
741 */
742 scrollToSlide( slideElement ) {
743
744 // If the scroll view isn't active yet, queue this action
745 if( !this.active ) {
746 this.activatedCallbacks.push( () => this.scrollToSlide( slideElement ) );
747 }
748 else {
749 // Find the trigger for this slide
750 const trigger = this.getScrollTriggerBySlide( slideElement );
751
752 if( trigger ) {
753 // Use the trigger's range to calculate the scroll position
754 this.viewportElement.scrollTop = trigger.range[0] * ( this.viewportElement.scrollHeight - this.viewportElement.offsetHeight );
755 }
756 }
757
758 }
759
760 /**
761 * Persists the current scroll position to session storage
762 * so that it can be restored.
763 */
764 storeScrollPosition() {
765
766 clearTimeout( this.storeScrollPositionTimeout );
767
768 this.storeScrollPositionTimeout = setTimeout( () => {
769 sessionStorage.setItem( 'reveal-scroll-top', this.viewportElement.scrollTop );
770 sessionStorage.setItem( 'reveal-scroll-origin', location.origin + location.pathname );
771
772 this.storeScrollPositionTimeout = null;
773 }, 50 );
774
775 }
776
777 /**
778 * Restores the scroll position when a deck is reloader.
779 */
780 restoreScrollPosition() {
781
782 const scrollPosition = sessionStorage.getItem( 'reveal-scroll-top' );
783 const scrollOrigin = sessionStorage.getItem( 'reveal-scroll-origin' );
784
785 if( scrollPosition && scrollOrigin === location.origin + location.pathname ) {
786 this.viewportElement.scrollTop = parseInt( scrollPosition, 10 );
787 }
788
789 }
790
791 /**
792 * Activates the given page and starts its embedded content
793 * if there is any.
794 *
795 * @param {object} page
796 */
797 activatePage( page ) {
798
799 if( !page.active ) {
800
801 page.active = true;
802
803 const { slideElement, backgroundElement, contentElement, indexh, indexv } = page;
804
805 contentElement.style.display = 'block';
806
807 slideElement.classList.add( 'present' );
808
809 if( backgroundElement ) {
810 backgroundElement.classList.add( 'present' );
811 }
812
813 this.Reveal.setCurrentScrollPage( slideElement, indexh, indexv );
814 this.Reveal.backgrounds.bubbleSlideContrastClassToElement( slideElement, this.viewportElement );
815
816 // If this page is part of an auto-animation there will be one
817 // content element per auto-animated page. We need to show the
818 // current page and hide all others.
819 Array.from( contentElement.parentNode.querySelectorAll( '.scroll-page-content' ) ).forEach( sibling => {
820 if( sibling !== contentElement ) {
821 sibling.style.display = 'none';
822 }
823 });
824
825 }
826
827 }
828
829 /**
830 * Deactivates the page after it has been visible.
831 *
832 * @param {object} page
833 */
834 deactivatePage( page ) {
835
836 if( page.active ) {
837
838 page.active = false;
839 if( page.slideElement ) page.slideElement.classList.remove( 'present' );
840 if( page.backgroundElement ) page.backgroundElement.classList.remove( 'present' );
841
842 }
843
844 }
845
846 activateTrigger( trigger ) {
847
848 if( !trigger.active ) {
849 trigger.active = true;
850 trigger.activate();
851 }
852
853 }
854
855 deactivateTrigger( trigger ) {
856
857 if( trigger.active ) {
858 trigger.active = false;
859
860 if( trigger.deactivate ) {
861 trigger.deactivate();
862 }
863 }
864
865 }
866
867 /**
868 * Retrieve a slide by its original h/v index (i.e. the indices the
869 * slide had before being linearized).
870 *
871 * @param {number} h
872 * @param {number} v
873 * @returns {HTMLElement}
874 */
875 getSlideByIndices( h, v ) {
876
877 const page = this.getAllPages().find( page => {
878 return page.indexh === h && page.indexv === v;
879 } );
880
881 return page ? page.slideElement : null;
882
883 }
884
885 /**
886 * Retrieve a list of all scroll triggers for the given slide
887 * DOM element.
888 *
889 * @param {HTMLElement} slide
890 * @returns {Array}
891 */
892 getScrollTriggerBySlide( slide ) {
893
894 return this.slideTriggers.find( trigger => trigger.page.slideElement === slide );
895
896 }
897
898 /**
899 * Get a list of all pages in the scroll view. This includes
900 * both top-level slides and auto-animate steps.
901 *
902 * @returns {Array}
903 */
904 getAllPages() {
905
906 return this.pages.flatMap( page => [page, ...(page.autoAnimatePages || [])] );
907
908 }
909
910 onScroll() {
911
912 this.syncScrollPosition();
913 this.storeScrollPosition();
914
915 }
916
917 get viewportElement() {
918
919 return this.Reveal.getViewportElement();
920
921 }
922
923}