scrollview.js 26 KB

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