blob: fcdf60101e798b947b66f7562ea5bd3740493ca6 [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
452 }
453
454 /**
455 * Creates one scroll trigger for each fragments in the given page.
456 *
457 * @param {*} page
458 */
459 createFragmentTriggersForPage( page, slideElement ) {
460
461 slideElement = slideElement || page.slideElement;
462
463 // Each fragment 'group' is an array containing one or more
464 // fragments. Multiple fragments that appear at the same time
465 // are part of the same group.
466 const fragmentGroups = this.Reveal.fragments.sort( slideElement.querySelectorAll( '.fragment' ), true );
467
468 // Create scroll triggers that show/hide fragments
469 if( fragmentGroups.length ) {
470 page.fragments = this.Reveal.fragments.sort( slideElement.querySelectorAll( '.fragment:not(.disabled)' ) );
471 page.scrollTriggers.push(
472 // Trigger for the initial state with no fragments visible
473 {
474 activate: () => {
475 this.Reveal.fragments.update( -1, page.fragments, slideElement );
476 }
477 }
478 );
479
480 // Triggers for each fragment group
481 fragmentGroups.forEach( ( fragments, i ) => {
482 page.scrollTriggers.push({
483 activate: () => {
484 this.Reveal.fragments.update( i, page.fragments, slideElement );
485 }
486 });
487 } );
488 }
489
490
491 return page.scrollTriggers.length;
492
493 }
494
495 /**
496 * Creates scroll triggers for the auto-animate steps in the
497 * given page.
498 *
499 * @param {*} page
500 */
501 createAutoAnimateTriggersForPage( page ) {
502
503 if( page.autoAnimateElements.length > 0 ) {
504
505 // Triggers for each subsequent auto-animate slide
506 this.slideTriggers.push( ...Array.from( page.autoAnimateElements ).map( ( autoAnimateElement, i ) => {
507 let autoAnimatePage = this.createPage({
508 slideElement: autoAnimateElement.querySelector( 'section' ),
509 contentElement: autoAnimateElement,
510 backgroundElement: autoAnimateElement.querySelector( '.slide-background' )
511 });
512
513 // Create fragment scroll triggers for the auto-animate slide
514 this.createFragmentTriggersForPage( autoAnimatePage, autoAnimatePage.slideElement );
515
516 page.autoAnimatePages.push( autoAnimatePage );
517
518 // Return our slide trigger
519 return {
520 page: autoAnimatePage,
521 activate: () => this.activatePage( autoAnimatePage ),
522 deactivate: () => this.deactivatePage( autoAnimatePage )
523 };
524 }));
525 }
526
527 }
528
529 /**
530 * Helper method for creating a page definition and adding
531 * required fields. A "page" is a slide or auto-animate step.
532 */
533 createPage( page ) {
534
535 page.scrollTriggers = [];
536 page.indexh = parseInt( page.slideElement.getAttribute( 'data-index-h' ), 10 );
537 page.indexv = parseInt( page.slideElement.getAttribute( 'data-index-v' ), 10 );
538
539 return page;
540
541 }
542
543 /**
544 * Rerenders progress bar segments so that they match the current
545 * reveal.js config and size.
546 */
547 syncProgressBar() {
548
549 this.progressBarInner.querySelectorAll( '.scrollbar-slide' ).forEach( slide => slide.remove() );
550
551 const scrollHeight = this.viewportElement.scrollHeight;
552 const viewportHeight = this.viewportElement.offsetHeight;
553 const viewportHeightFactor = viewportHeight / scrollHeight;
554
555 this.progressBarHeight = this.progressBarInner.offsetHeight;
556 this.playheadHeight = Math.max( viewportHeightFactor * this.progressBarHeight, MIN_PLAYHEAD_HEIGHT );
557 this.progressBarScrollableHeight = this.progressBarHeight - this.playheadHeight;
558
559 const progressSegmentHeight = viewportHeight / scrollHeight * this.progressBarHeight;
560 const spacing = Math.min( progressSegmentHeight / 8, MAX_PROGRESS_SPACING );
561
562 this.progressBarPlayhead.style.height = this.playheadHeight - spacing + 'px';
563
564 // Don't show individual segments if they're too small
565 if( progressSegmentHeight > MIN_PROGRESS_SEGMENT_HEIGHT ) {
566
567 this.slideTriggers.forEach( slideTrigger => {
568
569 const { page } = slideTrigger;
570
571 // Visual representation of a slide
572 page.progressBarSlide = document.createElement( 'div' );
573 page.progressBarSlide.className = 'scrollbar-slide';
574 page.progressBarSlide.style.top = slideTrigger.range[0] * this.progressBarHeight + 'px';
575 page.progressBarSlide.style.height = ( slideTrigger.range[1] - slideTrigger.range[0] ) * this.progressBarHeight - spacing + 'px';
576 page.progressBarSlide.classList.toggle( 'has-triggers', page.scrollTriggers.length > 0 );
577 this.progressBarInner.appendChild( page.progressBarSlide );
578
579 // Visual representations of each scroll trigger
580 page.scrollTriggerElements = page.scrollTriggers.map( ( trigger, i ) => {
581
582 const triggerElement = document.createElement( 'div' );
583 triggerElement.className = 'scrollbar-trigger';
584 triggerElement.style.top = ( trigger.range[0] - slideTrigger.range[0] ) * this.progressBarHeight + 'px';
585 triggerElement.style.height = ( trigger.range[1] - trigger.range[0] ) * this.progressBarHeight - spacing + 'px';
586 page.progressBarSlide.appendChild( triggerElement );
587
588 if( i === 0 ) triggerElement.style.display = 'none';
589
590 return triggerElement;
591
592 } );
593
594 } );
595
596 }
597 else {
598
599 this.pages.forEach( page => page.progressBarSlide = null );
600
601 }
602
603 }
604
605 /**
606 * Reads the current scroll position and updates our active
607 * trigger states accordingly.
608 */
609 syncScrollPosition() {
610
611 const viewportHeight = this.viewportElement.offsetHeight;
612 const viewportHeightFactor = viewportHeight / this.viewportElement.scrollHeight;
613
614 const scrollTop = this.viewportElement.scrollTop;
615 const scrollHeight = this.viewportElement.scrollHeight - viewportHeight
616 const scrollProgress = Math.max( Math.min( scrollTop / scrollHeight, 1 ), 0 );
617 const scrollProgressMid = Math.max( Math.min( ( scrollTop + viewportHeight / 2 ) / this.viewportElement.scrollHeight, 1 ), 0 );
618
619 let activePage;
620
621 this.slideTriggers.forEach( ( trigger ) => {
622 const { page } = trigger;
623
624 const shouldPreload = scrollProgress >= trigger.range[0] - viewportHeightFactor*2 &&
625 scrollProgress <= trigger.range[1] + viewportHeightFactor*2;
626
627 // Load slides that are within the preload range
628 if( shouldPreload && !page.loaded ) {
629 page.loaded = true;
630 this.Reveal.slideContent.load( page.slideElement );
631 }
632 else if( page.loaded ) {
633 page.loaded = false;
634 this.Reveal.slideContent.unload( page.slideElement );
635 }
636
637 // If we're within this trigger range, activate it
638 if( scrollProgress >= trigger.range[0] && scrollProgress <= trigger.range[1] ) {
639 this.activateTrigger( trigger );
640 activePage = trigger.page;
641 }
642 // .. otherwise deactivate
643 else if( trigger.active ) {
644 this.deactivateTrigger( trigger );
645 }
646 } );
647
648 // Each page can have its own scroll triggers, check if any of those
649 // need to be activated/deactivated
650 if( activePage ) {
651 activePage.scrollTriggers.forEach( ( trigger ) => {
652 if( scrollProgressMid >= trigger.range[0] && scrollProgressMid <= trigger.range[1] ) {
653 this.activateTrigger( trigger );
654 }
655 else if( trigger.active ) {
656 this.deactivateTrigger( trigger );
657 }
658 } );
659 }
660
661 // Update our visual progress indication
662 this.setProgressBarValue( scrollTop / ( this.viewportElement.scrollHeight - viewportHeight ) );
663
664 }
665
666 /**
667 * Moves the progress bar playhead to the specified position.
668 *
669 * @param {number} progress 0-1
670 */
671 setProgressBarValue( progress ) {
672
673 if( this.progressBar ) {
674
675 this.progressBarPlayhead.style.transform = `translateY(${progress * this.progressBarScrollableHeight}px)`;
676
677 this.getAllPages()
678 .filter( page => page.progressBarSlide )
679 .forEach( ( page ) => {
680 page.progressBarSlide.classList.toggle( 'active', page.active === true );
681
682 page.scrollTriggers.forEach( ( trigger, i ) => {
683 page.scrollTriggerElements[i].classList.toggle( 'active', page.active === true && trigger.active === true );
684 } );
685 } );
686
687 this.showProgressBar();
688
689 }
690
691 }
692
693 /**
694 * Show the progress bar and, if configured, automatically hide
695 * it after a delay.
696 */
697 showProgressBar() {
698
699 this.progressBar.classList.add( 'visible' );
700
701 clearTimeout( this.hideProgressBarTimeout );
702
703 if( this.Reveal.getConfig().scrollProgress === 'auto' && !this.draggingProgressBar ) {
704
705 this.hideProgressBarTimeout = setTimeout( () => {
706 if( this.progressBar ) {
707 this.progressBar.classList.remove( 'visible' );
708 }
709 }, HIDE_SCROLLBAR_TIMEOUT );
710
711 }
712
713 }
714
715 /**
716 * Scroll to the previous page.
717 */
718 prev() {
719
720 this.viewportElement.scrollTop -= this.scrollTriggerHeight;
721
722 }
723
724 /**
725 * Scroll to the next page.
726 */
727 next() {
728
729 this.viewportElement.scrollTop += this.scrollTriggerHeight;
730
731 }
732
733 /**
734 * Scrolls the given slide element into view.
735 *
736 * @param {HTMLElement} slideElement
737 */
738 scrollToSlide( slideElement ) {
739
740 // If the scroll view isn't active yet, queue this action
741 if( !this.active ) {
742 this.activatedCallbacks.push( () => this.scrollToSlide( slideElement ) );
743 }
744 else {
745 // Find the trigger for this slide
746 const trigger = this.getScrollTriggerBySlide( slideElement );
747
748 if( trigger ) {
749 // Use the trigger's range to calculate the scroll position
750 this.viewportElement.scrollTop = trigger.range[0] * ( this.viewportElement.scrollHeight - this.viewportElement.offsetHeight );
751 }
752 }
753
754 }
755
756 /**
757 * Persists the current scroll position to session storage
758 * so that it can be restored.
759 */
760 storeScrollPosition() {
761
762 clearTimeout( this.storeScrollPositionTimeout );
763
764 this.storeScrollPositionTimeout = setTimeout( () => {
765 sessionStorage.setItem( 'reveal-scroll-top', this.viewportElement.scrollTop );
766 sessionStorage.setItem( 'reveal-scroll-origin', location.origin + location.pathname );
767
768 this.storeScrollPositionTimeout = null;
769 }, 50 );
770
771 }
772
773 /**
774 * Restores the scroll position when a deck is reloader.
775 */
776 restoreScrollPosition() {
777
778 const scrollPosition = sessionStorage.getItem( 'reveal-scroll-top' );
779 const scrollOrigin = sessionStorage.getItem( 'reveal-scroll-origin' );
780
781 if( scrollPosition && scrollOrigin === location.origin + location.pathname ) {
782 this.viewportElement.scrollTop = parseInt( scrollPosition, 10 );
783 }
784
785 }
786
787 /**
788 * Activates the given page and starts its embedded content
789 * if there is any.
790 *
791 * @param {object} page
792 */
793 activatePage( page ) {
794
795 if( !page.active ) {
796
797 page.active = true;
798
799 const { slideElement, backgroundElement, contentElement, indexh, indexv } = page;
800
801 contentElement.style.display = 'block';
802
803 slideElement.classList.add( 'present' );
804
805 if( backgroundElement ) {
806 backgroundElement.classList.add( 'present' );
807 }
808
809 this.Reveal.setCurrentScrollPage( slideElement, indexh, indexv );
810 this.Reveal.backgrounds.bubbleSlideContrastClassToElement( slideElement, this.viewportElement );
811
812 // If this page is part of an auto-animation there will be one
813 // content element per auto-animated page. We need to show the
814 // current page and hide all others.
815 Array.from( contentElement.parentNode.querySelectorAll( '.scroll-page-content' ) ).forEach( sibling => {
816 if( sibling !== contentElement ) {
817 sibling.style.display = 'none';
818 }
819 });
820
821 }
822
823 }
824
825 /**
826 * Deactivates the page after it has been visible.
827 *
828 * @param {object} page
829 */
830 deactivatePage( page ) {
831
832 if( page.active ) {
833
834 page.active = false;
835 if( page.slideElement ) page.slideElement.classList.remove( 'present' );
836 if( page.backgroundElement ) page.backgroundElement.classList.remove( 'present' );
837
838 }
839
840 }
841
842 activateTrigger( trigger ) {
843
844 if( !trigger.active ) {
845 trigger.active = true;
846 trigger.activate();
847 }
848
849 }
850
851 deactivateTrigger( trigger ) {
852
853 if( trigger.active ) {
854 trigger.active = false;
855
856 if( trigger.deactivate ) {
857 trigger.deactivate();
858 }
859 }
860
861 }
862
863 /**
864 * Retrieve a slide by its original h/v index (i.e. the indices the
865 * slide had before being linearized).
866 *
867 * @param {number} h
868 * @param {number} v
869 * @returns {HTMLElement}
870 */
871 getSlideByIndices( h, v ) {
872
873 const page = this.getAllPages().find( page => {
874 return page.indexh === h && page.indexv === v;
875 } );
876
877 return page ? page.slideElement : null;
878
879 }
880
881 /**
882 * Retrieve a list of all scroll triggers for the given slide
883 * DOM element.
884 *
885 * @param {HTMLElement} slide
886 * @returns {Array}
887 */
888 getScrollTriggerBySlide( slide ) {
889
890 return this.slideTriggers.find( trigger => trigger.page.slideElement === slide );
891
892 }
893
894 /**
895 * Get a list of all pages in the scroll view. This includes
896 * both top-level slides and auto-animate steps.
897 *
898 * @returns {Array}
899 */
900 getAllPages() {
901
902 return this.pages.flatMap( page => [page, ...(page.autoAnimatePages || [])] );
903
904 }
905
906 onScroll() {
907
908 this.syncScrollPosition();
909 this.storeScrollPosition();
910
911 }
912
913 get viewportElement() {
914
915 return this.Reveal.getViewportElement();
916
917 }
918
919}