update reveal.js
This commit is contained in:
@@ -15,7 +15,10 @@ export default {
|
||||
minScale: 0.2,
|
||||
maxScale: 2.0,
|
||||
|
||||
// Display presentation control arrows
|
||||
// Display presentation control arrows.
|
||||
// - true: Display controls on all screens
|
||||
// - false: Hide controls on all screens
|
||||
// - "speaker-only": Only display controls in the speaker view
|
||||
controls: true,
|
||||
|
||||
// Help the user learn the controls by providing hints, for example by
|
||||
@@ -65,13 +68,16 @@ export default {
|
||||
// Flags if we should monitor the hash and change slides accordingly
|
||||
respondToHashChanges: true,
|
||||
|
||||
// Enable support for jump-to-slide navigation shortcuts
|
||||
jumpToSlide: true,
|
||||
|
||||
// Push each slide change to the browser history. Implies `hash: true`
|
||||
history: false,
|
||||
|
||||
// Enable keyboard shortcuts for navigation
|
||||
keyboard: true,
|
||||
|
||||
// Optional function that blocks keyboard events when retuning false
|
||||
// Optional function that blocks keyboard events when returning false
|
||||
//
|
||||
// If you set this to 'focused', we will only capture keyboard events
|
||||
// for embedded decks when they are in focus
|
||||
@@ -253,6 +259,36 @@ export default {
|
||||
parallaxBackgroundHorizontal: null,
|
||||
parallaxBackgroundVertical: null,
|
||||
|
||||
// Can be used to initialize reveal.js in one of the following views:
|
||||
// - print: Render the presentation so that it can be printed to PDF
|
||||
// - scroll: Show the presentation as a tall scrollable page with scroll
|
||||
// triggered animations
|
||||
view: null,
|
||||
|
||||
// Adjusts the height of each slide in the scroll view.
|
||||
// - full: Each slide is as tall as the viewport
|
||||
// - compact: Slides are as small as possible, allowing multiple slides
|
||||
// to be visible in parallel on tall devices
|
||||
scrollLayout: 'full',
|
||||
|
||||
// Control how scroll snapping works in the scroll view.
|
||||
// - false: No snapping, scrolling is continuous
|
||||
// - proximity: Snap when close to a slide
|
||||
// - mandatory: Always snap to the closest slide
|
||||
//
|
||||
// Only applies to presentations in scroll view.
|
||||
scrollSnap: 'mandatory',
|
||||
|
||||
// Enables and configure the scroll view progress bar.
|
||||
// - 'auto': Show the scrollbar while scrolling, hide while idle
|
||||
// - true: Always show the scrollbar
|
||||
// - false: Never show the scrollbar
|
||||
scrollProgress: 'auto',
|
||||
|
||||
// Automatically activate the scroll view when we the viewport falls
|
||||
// below the given width.
|
||||
scrollActivationWidth: 435,
|
||||
|
||||
// The maximum number of pages a single slide can expand onto when printing
|
||||
// to PDF, unlimited by default
|
||||
pdfMaxPagesPerSlide: Number.POSITIVE_INFINITY,
|
||||
@@ -284,6 +320,10 @@ export default {
|
||||
// Time before the cursor is hidden (in ms)
|
||||
hideCursorTime: 5000,
|
||||
|
||||
// Should we automatically sort and set indices for fragments
|
||||
// at each sync? (See Reveal.sync)
|
||||
sortFragmentsOnSync: true,
|
||||
|
||||
// Script dependencies to load
|
||||
dependencies: [],
|
||||
|
||||
|
||||
@@ -31,10 +31,13 @@ export default class AutoAnimate {
|
||||
let toSlideIndex = allSlides.indexOf( toSlide );
|
||||
let fromSlideIndex = allSlides.indexOf( fromSlide );
|
||||
|
||||
// Ensure that both slides are auto-animate targets with the same data-auto-animate-id value
|
||||
// (including null if absent on both) and that data-auto-animate-restart isn't set on the
|
||||
// physically latter slide (independent of slide direction)
|
||||
if( fromSlide.hasAttribute( 'data-auto-animate' ) && toSlide.hasAttribute( 'data-auto-animate' )
|
||||
// Ensure that;
|
||||
// 1. Both slides exist.
|
||||
// 2. Both slides are auto-animate targets with the same
|
||||
// data-auto-animate-id value (including null if absent on both).
|
||||
// 3. data-auto-animate-restart isn't set on the physically latter
|
||||
// slide (independent of slide direction).
|
||||
if( fromSlide && toSlide && fromSlide.hasAttribute( 'data-auto-animate' ) && toSlide.hasAttribute( 'data-auto-animate' )
|
||||
&& fromSlide.getAttribute( 'data-auto-animate-id' ) === toSlide.getAttribute( 'data-auto-animate-id' )
|
||||
&& !( toSlideIndex > fromSlideIndex ? toSlide : fromSlide ).hasAttribute( 'data-auto-animate-restart' ) ) {
|
||||
|
||||
@@ -50,11 +53,19 @@ export default class AutoAnimate {
|
||||
// Flag the navigation direction, needed for fragment buildup
|
||||
animationOptions.slideDirection = toSlideIndex > fromSlideIndex ? 'forward' : 'backward';
|
||||
|
||||
// If the from-slide is hidden because it has moved outside
|
||||
// the view distance, we need to temporarily show it while
|
||||
// measuring
|
||||
let fromSlideIsHidden = fromSlide.style.display === 'none';
|
||||
if( fromSlideIsHidden ) fromSlide.style.display = this.Reveal.getConfig().display;
|
||||
|
||||
// Inject our auto-animate styles for this transition
|
||||
let css = this.getAutoAnimatableElements( fromSlide, toSlide ).map( elements => {
|
||||
return this.autoAnimateElements( elements.from, elements.to, elements.options || {}, animationOptions, autoAnimateCounter++ );
|
||||
} );
|
||||
|
||||
if( fromSlideIsHidden ) fromSlide.style.display = 'none';
|
||||
|
||||
// Animate unmatched elements, if enabled
|
||||
if( toSlide.dataset.autoAnimateUnmatched !== 'false' && this.Reveal.getConfig().autoAnimateUnmatched === true ) {
|
||||
|
||||
@@ -167,28 +178,12 @@ export default class AutoAnimate {
|
||||
let fromProps = this.getAutoAnimatableProperties( 'from', from, elementOptions ),
|
||||
toProps = this.getAutoAnimatableProperties( 'to', to, elementOptions );
|
||||
|
||||
// Maintain fragment visibility for matching elements when
|
||||
// we're navigating forwards, this way the viewer won't need
|
||||
// to step through the same fragments twice
|
||||
if( to.classList.contains( 'fragment' ) ) {
|
||||
|
||||
// Don't auto-animate the opacity of fragments to avoid
|
||||
// conflicts with fragment animations
|
||||
delete toProps.styles['opacity'];
|
||||
|
||||
if( from.classList.contains( 'fragment' ) ) {
|
||||
|
||||
let fromFragmentStyle = ( from.className.match( FRAGMENT_STYLE_REGEX ) || [''] )[0];
|
||||
let toFragmentStyle = ( to.className.match( FRAGMENT_STYLE_REGEX ) || [''] )[0];
|
||||
|
||||
// Only skip the fragment if the fragment animation style
|
||||
// remains unchanged
|
||||
if( fromFragmentStyle === toFragmentStyle && animationOptions.slideDirection === 'forward' ) {
|
||||
to.classList.add( 'visible', 'disabled' );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// If translation and/or scaling are enabled, css transform
|
||||
@@ -391,7 +386,14 @@ export default class AutoAnimate {
|
||||
value = { value: style.to, explicitValue: true };
|
||||
}
|
||||
else {
|
||||
value = computedStyles[style.property];
|
||||
// Use a unitless value for line-height so that it inherits properly
|
||||
if( style.property === 'line-height' ) {
|
||||
value = parseFloat( computedStyles['line-height'] ) / parseFloat( computedStyles['font-size'] );
|
||||
}
|
||||
|
||||
if( isNaN(value) ) {
|
||||
value = computedStyles[style.property];
|
||||
}
|
||||
}
|
||||
|
||||
if( value !== '' ) {
|
||||
@@ -446,14 +448,14 @@ export default class AutoAnimate {
|
||||
const textNodes = 'h1, h2, h3, h4, h5, h6, p, li';
|
||||
const mediaNodes = 'img, video, iframe';
|
||||
|
||||
// Eplicit matches via data-id
|
||||
// Explicit matches via data-id
|
||||
this.findAutoAnimateMatches( pairs, fromSlide, toSlide, '[data-id]', node => {
|
||||
return node.nodeName + ':::' + node.getAttribute( 'data-id' );
|
||||
} );
|
||||
|
||||
// Text
|
||||
this.findAutoAnimateMatches( pairs, fromSlide, toSlide, textNodes, node => {
|
||||
return node.nodeName + ':::' + node.innerText;
|
||||
return node.nodeName + ':::' + node.textContent.trim();
|
||||
} );
|
||||
|
||||
// Media
|
||||
@@ -463,11 +465,10 @@ export default class AutoAnimate {
|
||||
|
||||
// Code
|
||||
this.findAutoAnimateMatches( pairs, fromSlide, toSlide, codeNodes, node => {
|
||||
return node.nodeName + ':::' + node.innerText;
|
||||
return node.nodeName + ':::' + node.textContent.trim();
|
||||
} );
|
||||
|
||||
pairs.forEach( pair => {
|
||||
|
||||
// Disable scale transformations on text nodes, we transition
|
||||
// each individual text property instead
|
||||
if( matches( pair.from, textNodes ) ) {
|
||||
@@ -490,7 +491,7 @@ export default class AutoAnimate {
|
||||
} );
|
||||
|
||||
// Line numbers
|
||||
this.findAutoAnimateMatches( pairs, pair.from, pair.to, '.hljs .hljs-ln-line[data-line-number]', node => {
|
||||
this.findAutoAnimateMatches( pairs, pair.from, pair.to, '.hljs .hljs-ln-numbers[data-line-number]', node => {
|
||||
return node.getAttribute( 'data-line-number' );
|
||||
}, {
|
||||
scale: false,
|
||||
@@ -559,14 +560,14 @@ export default class AutoAnimate {
|
||||
|
||||
// Retrieve the 'from' element
|
||||
if( fromMatches[key] ) {
|
||||
const pimaryIndex = toMatches[key].length - 1;
|
||||
const primaryIndex = toMatches[key].length - 1;
|
||||
const secondaryIndex = fromMatches[key].length - 1;
|
||||
|
||||
// If there are multiple identical from elements, retrieve
|
||||
// the one at the same index as our to-element.
|
||||
if( fromMatches[key][ pimaryIndex ] ) {
|
||||
fromElement = fromMatches[key][ pimaryIndex ];
|
||||
fromMatches[key][ pimaryIndex ] = null;
|
||||
if( fromMatches[key][ primaryIndex ] ) {
|
||||
fromElement = fromMatches[key][ primaryIndex ];
|
||||
fromMatches[key][ primaryIndex ] = null;
|
||||
}
|
||||
// If there are no matching from-elements at the same index,
|
||||
// use the last one.
|
||||
@@ -594,7 +595,7 @@ export default class AutoAnimate {
|
||||
* fading of unmatched elements is turned on, these elements
|
||||
* will fade when going between auto-animate slides.
|
||||
*
|
||||
* Note that parents of auto-animate targets are NOT considerd
|
||||
* Note that parents of auto-animate targets are NOT considered
|
||||
* unmatched since fading them would break the auto-animation.
|
||||
*
|
||||
* @param {HTMLElement} rootElement
|
||||
|
||||
@@ -122,6 +122,7 @@ export default class Backgrounds {
|
||||
backgroundVideo: slide.getAttribute( 'data-background-video' ),
|
||||
backgroundIframe: slide.getAttribute( 'data-background-iframe' ),
|
||||
backgroundColor: slide.getAttribute( 'data-background-color' ),
|
||||
backgroundGradient: slide.getAttribute( 'data-background-gradient' ),
|
||||
backgroundRepeat: slide.getAttribute( 'data-background-repeat' ),
|
||||
backgroundPosition: slide.getAttribute( 'data-background-position' ),
|
||||
backgroundTransition: slide.getAttribute( 'data-background-transition' ),
|
||||
@@ -150,7 +151,7 @@ export default class Backgrounds {
|
||||
|
||||
if( data.background ) {
|
||||
// Auto-wrap image urls in url(...)
|
||||
if( /^(http|file|\/\/)/gi.test( data.background ) || /\.(svg|png|jpg|jpeg|gif|bmp)([?#\s]|$)/gi.test( data.background ) ) {
|
||||
if( /^(http|file|\/\/)/gi.test( data.background ) || /\.(svg|png|jpg|jpeg|gif|bmp|webp)([?#\s]|$)/gi.test( data.background ) ) {
|
||||
slide.setAttribute( 'data-background-image', data.background );
|
||||
}
|
||||
else {
|
||||
@@ -161,13 +162,14 @@ export default class Backgrounds {
|
||||
// Create a hash for this combination of background settings.
|
||||
// This is used to determine when two slide backgrounds are
|
||||
// the same.
|
||||
if( data.background || data.backgroundColor || data.backgroundImage || data.backgroundVideo || data.backgroundIframe ) {
|
||||
if( data.background || data.backgroundColor || data.backgroundGradient || data.backgroundImage || data.backgroundVideo || data.backgroundIframe ) {
|
||||
element.setAttribute( 'data-background-hash', data.background +
|
||||
data.backgroundSize +
|
||||
data.backgroundImage +
|
||||
data.backgroundVideo +
|
||||
data.backgroundIframe +
|
||||
data.backgroundColor +
|
||||
data.backgroundGradient +
|
||||
data.backgroundRepeat +
|
||||
data.backgroundPosition +
|
||||
data.backgroundTransition +
|
||||
@@ -177,6 +179,7 @@ export default class Backgrounds {
|
||||
// Additional and optional background properties
|
||||
if( data.backgroundSize ) element.setAttribute( 'data-background-size', data.backgroundSize );
|
||||
if( data.backgroundColor ) element.style.backgroundColor = data.backgroundColor;
|
||||
if( data.backgroundGradient ) element.style.backgroundImage = data.backgroundGradient;
|
||||
if( data.backgroundTransition ) element.setAttribute( 'data-background-transition', data.backgroundTransition );
|
||||
|
||||
if( dataPreload ) element.setAttribute( 'data-preload', '' );
|
||||
@@ -187,10 +190,30 @@ export default class Backgrounds {
|
||||
if( data.backgroundPosition ) contentElement.style.backgroundPosition = data.backgroundPosition;
|
||||
if( data.backgroundOpacity ) contentElement.style.opacity = data.backgroundOpacity;
|
||||
|
||||
const contrastClass = this.getContrastClass( slide );
|
||||
|
||||
if( typeof contrastClass === 'string' ) {
|
||||
slide.classList.add( contrastClass );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a class name that can be applied to a slide to indicate
|
||||
* if it has a light or dark background.
|
||||
*
|
||||
* @param {*} slide
|
||||
*
|
||||
* @returns {string|null}
|
||||
*/
|
||||
getContrastClass( slide ) {
|
||||
|
||||
const element = slide.slideBackgroundElement;
|
||||
|
||||
// If this slide has a background color, we add a class that
|
||||
// signals if it is light or dark. If the slide has no background
|
||||
// color, no class will be added
|
||||
let contrastColor = data.backgroundColor;
|
||||
let contrastColor = slide.getAttribute( 'data-background-color' );
|
||||
|
||||
// If no bg color was found, or it cannot be converted by colorToRgb, check the computed background
|
||||
if( !contrastColor || !colorToRgb( contrastColor ) ) {
|
||||
@@ -208,14 +231,32 @@ export default class Backgrounds {
|
||||
// an element with no background
|
||||
if( rgb && rgb.a !== 0 ) {
|
||||
if( colorBrightness( contrastColor ) < 128 ) {
|
||||
slide.classList.add( 'has-dark-background' );
|
||||
return 'has-dark-background';
|
||||
}
|
||||
else {
|
||||
slide.classList.add( 'has-light-background' );
|
||||
return 'has-light-background';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Bubble the 'has-light-background'/'has-dark-background' classes.
|
||||
*/
|
||||
bubbleSlideContrastClassToElement( slide, target ) {
|
||||
|
||||
[ 'has-light-background', 'has-dark-background' ].forEach( classToBubble => {
|
||||
if( slide.classList.contains( classToBubble ) ) {
|
||||
target.classList.add( classToBubble );
|
||||
}
|
||||
else {
|
||||
target.classList.remove( classToBubble );
|
||||
}
|
||||
}, this );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -227,14 +268,15 @@ export default class Backgrounds {
|
||||
*/
|
||||
update( includeAll = false ) {
|
||||
|
||||
let config = this.Reveal.getConfig();
|
||||
let currentSlide = this.Reveal.getCurrentSlide();
|
||||
let indices = this.Reveal.getIndices();
|
||||
|
||||
let currentBackground = null;
|
||||
|
||||
// Reverse past/future classes when in RTL mode
|
||||
let horizontalPast = this.Reveal.getConfig().rtl ? 'future' : 'past',
|
||||
horizontalFuture = this.Reveal.getConfig().rtl ? 'past' : 'future';
|
||||
let horizontalPast = config.rtl ? 'future' : 'past',
|
||||
horizontalFuture = config.rtl ? 'past' : 'future';
|
||||
|
||||
// Update the classes of all backgrounds to match the
|
||||
// states of their slides (past/present/future)
|
||||
@@ -260,10 +302,12 @@ export default class Backgrounds {
|
||||
|
||||
backgroundv.classList.remove( 'past', 'present', 'future' );
|
||||
|
||||
if( v < indices.v ) {
|
||||
const indexv = typeof indices.v === 'number' ? indices.v : 0;
|
||||
|
||||
if( v < indexv ) {
|
||||
backgroundv.classList.add( 'past' );
|
||||
}
|
||||
else if ( v > indices.v ) {
|
||||
else if ( v > indexv ) {
|
||||
backgroundv.classList.add( 'future' );
|
||||
}
|
||||
else {
|
||||
@@ -278,15 +322,53 @@ export default class Backgrounds {
|
||||
|
||||
} );
|
||||
|
||||
// The previous background may refer to a DOM element that has
|
||||
// been removed after a presentation is synced & bgs are recreated
|
||||
if( this.previousBackground && !this.previousBackground.closest( 'body' ) ) {
|
||||
this.previousBackground = null;
|
||||
}
|
||||
|
||||
if( currentBackground && this.previousBackground ) {
|
||||
|
||||
// Don't transition between identical backgrounds. This
|
||||
// prevents unwanted flicker.
|
||||
let previousBackgroundHash = this.previousBackground.getAttribute( 'data-background-hash' );
|
||||
let currentBackgroundHash = currentBackground.getAttribute( 'data-background-hash' );
|
||||
|
||||
if( currentBackgroundHash && currentBackgroundHash === previousBackgroundHash && currentBackground !== this.previousBackground ) {
|
||||
this.element.classList.add( 'no-transition' );
|
||||
|
||||
// If multiple slides have the same background video, carry
|
||||
// the <video> element forward so that it plays continuously
|
||||
// across multiple slides
|
||||
const currentVideo = currentBackground.querySelector( 'video' );
|
||||
const previousVideo = this.previousBackground.querySelector( 'video' );
|
||||
|
||||
if( currentVideo && previousVideo ) {
|
||||
|
||||
const currentVideoParent = currentVideo.parentNode;
|
||||
const previousVideoParent = previousVideo.parentNode;
|
||||
|
||||
// Swap the two videos
|
||||
previousVideoParent.appendChild( currentVideo );
|
||||
currentVideoParent.appendChild( previousVideo );
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const backgroundChanged = currentBackground !== this.previousBackground;
|
||||
|
||||
// Stop content inside of previous backgrounds
|
||||
if( this.previousBackground ) {
|
||||
if( backgroundChanged && this.previousBackground ) {
|
||||
|
||||
this.Reveal.slideContent.stopEmbeddedContent( this.previousBackground, { unloadIframes: !this.Reveal.slideContent.shouldPreload( this.previousBackground ) } );
|
||||
|
||||
}
|
||||
|
||||
// Start content in the current background
|
||||
if( currentBackground ) {
|
||||
if( backgroundChanged && currentBackground ) {
|
||||
|
||||
this.Reveal.slideContent.startEmbeddedContent( currentBackground );
|
||||
|
||||
@@ -304,14 +386,6 @@ export default class Backgrounds {
|
||||
|
||||
}
|
||||
|
||||
// Don't transition between identical backgrounds. This
|
||||
// prevents unwanted flicker.
|
||||
let previousBackgroundHash = this.previousBackground ? this.previousBackground.getAttribute( 'data-background-hash' ) : null;
|
||||
let currentBackgroundHash = currentBackground.getAttribute( 'data-background-hash' );
|
||||
if( currentBackgroundHash && currentBackgroundHash === previousBackgroundHash && currentBackground !== this.previousBackground ) {
|
||||
this.element.classList.add( 'no-transition' );
|
||||
}
|
||||
|
||||
this.previousBackground = currentBackground;
|
||||
|
||||
}
|
||||
@@ -319,20 +393,13 @@ export default class Backgrounds {
|
||||
// If there's a background brightness flag for this slide,
|
||||
// bubble it to the .reveal container
|
||||
if( currentSlide ) {
|
||||
[ 'has-light-background', 'has-dark-background' ].forEach( classToBubble => {
|
||||
if( currentSlide.classList.contains( classToBubble ) ) {
|
||||
this.Reveal.getRevealElement().classList.add( classToBubble );
|
||||
}
|
||||
else {
|
||||
this.Reveal.getRevealElement().classList.remove( classToBubble );
|
||||
}
|
||||
}, this );
|
||||
this.bubbleSlideContrastClassToElement( currentSlide, this.Reveal.getRevealElement() );
|
||||
}
|
||||
|
||||
// Allow the first background to apply without transition
|
||||
setTimeout( () => {
|
||||
this.element.classList.remove( 'no-transition' );
|
||||
}, 1 );
|
||||
}, 10 );
|
||||
|
||||
}
|
||||
|
||||
|
||||
27
scripts/reveal.js/js/controllers/controls.js
vendored
27
scripts/reveal.js/js/controllers/controls.js
vendored
@@ -1,4 +1,4 @@
|
||||
import { queryAll } from '../utils/util.js'
|
||||
import { queryAll, enterFullscreen } from '../utils/util.js'
|
||||
import { isAndroid } from '../utils/device.js'
|
||||
|
||||
/**
|
||||
@@ -12,6 +12,7 @@ import { isAndroid } from '../utils/device.js'
|
||||
* - .navigate-left
|
||||
* - .navigate-next
|
||||
* - .navigate-prev
|
||||
* - .enter-fullscreen
|
||||
*/
|
||||
export default class Controls {
|
||||
|
||||
@@ -25,6 +26,7 @@ export default class Controls {
|
||||
this.onNavigateDownClicked = this.onNavigateDownClicked.bind( this );
|
||||
this.onNavigatePrevClicked = this.onNavigatePrevClicked.bind( this );
|
||||
this.onNavigateNextClicked = this.onNavigateNextClicked.bind( this );
|
||||
this.onEnterFullscreen = this.onEnterFullscreen.bind( this );
|
||||
|
||||
}
|
||||
|
||||
@@ -50,6 +52,7 @@ export default class Controls {
|
||||
this.controlsDown = queryAll( revealElement, '.navigate-down' );
|
||||
this.controlsPrev = queryAll( revealElement, '.navigate-prev' );
|
||||
this.controlsNext = queryAll( revealElement, '.navigate-next' );
|
||||
this.controlsFullscreen = queryAll( revealElement, '.enter-fullscreen' );
|
||||
|
||||
// The left, right and down arrows in the standard reveal.js controls
|
||||
this.controlsRightArrow = this.element.querySelector( '.navigate-right' );
|
||||
@@ -63,7 +66,10 @@ export default class Controls {
|
||||
*/
|
||||
configure( config, oldConfig ) {
|
||||
|
||||
this.element.style.display = config.controls ? 'block' : 'none';
|
||||
this.element.style.display = (
|
||||
config.controls &&
|
||||
(config.controls !== 'speaker-only' || this.Reveal.isSpeakerNotes())
|
||||
) ? 'block' : 'none';
|
||||
|
||||
this.element.setAttribute( 'data-controls-layout', config.controlsLayout );
|
||||
this.element.setAttribute( 'data-controls-back-arrows', config.controlsBackArrows );
|
||||
@@ -89,6 +95,7 @@ export default class Controls {
|
||||
this.controlsDown.forEach( el => el.addEventListener( eventName, this.onNavigateDownClicked, false ) );
|
||||
this.controlsPrev.forEach( el => el.addEventListener( eventName, this.onNavigatePrevClicked, false ) );
|
||||
this.controlsNext.forEach( el => el.addEventListener( eventName, this.onNavigateNextClicked, false ) );
|
||||
this.controlsFullscreen.forEach( el => el.addEventListener( eventName, this.onEnterFullscreen, false ) );
|
||||
} );
|
||||
|
||||
}
|
||||
@@ -102,6 +109,7 @@ export default class Controls {
|
||||
this.controlsDown.forEach( el => el.removeEventListener( eventName, this.onNavigateDownClicked, false ) );
|
||||
this.controlsPrev.forEach( el => el.removeEventListener( eventName, this.onNavigatePrevClicked, false ) );
|
||||
this.controlsNext.forEach( el => el.removeEventListener( eventName, this.onNavigateNextClicked, false ) );
|
||||
this.controlsFullscreen.forEach( el => el.removeEventListener( eventName, this.onEnterFullscreen, false ) );
|
||||
} );
|
||||
|
||||
}
|
||||
@@ -141,9 +149,14 @@ export default class Controls {
|
||||
if( fragmentsRoutes.prev ) this.controlsPrev.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
|
||||
if( fragmentsRoutes.next ) this.controlsNext.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
|
||||
|
||||
const isVerticalStack = this.Reveal.isVerticalSlide( currentSlide );
|
||||
const hasVerticalSiblings = isVerticalStack &&
|
||||
currentSlide.parentElement &&
|
||||
currentSlide.parentElement.querySelectorAll( ':scope > section' ).length > 1;
|
||||
|
||||
// Apply fragment decorators to directional buttons based on
|
||||
// what slide axis they are in
|
||||
if( this.Reveal.isVerticalSlide( currentSlide ) ) {
|
||||
if( isVerticalStack && hasVerticalSiblings ) {
|
||||
if( fragmentsRoutes.prev ) this.controlsUp.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
|
||||
if( fragmentsRoutes.next ) this.controlsDown.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
|
||||
}
|
||||
@@ -262,5 +275,13 @@ export default class Controls {
|
||||
|
||||
}
|
||||
|
||||
onEnterFullscreen( event ) {
|
||||
|
||||
const config = this.Reveal.getConfig();
|
||||
const viewport = this.Reveal.getViewportElement();
|
||||
|
||||
enterFullscreen( config.embedded ? viewport : viewport.parentElement );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -174,24 +174,23 @@ export default class Fragments {
|
||||
*
|
||||
* @return {{shown: array, hidden: array}}
|
||||
*/
|
||||
update( index, fragments ) {
|
||||
update( index, fragments, slide = this.Reveal.getCurrentSlide() ) {
|
||||
|
||||
let changedFragments = {
|
||||
shown: [],
|
||||
hidden: []
|
||||
};
|
||||
|
||||
let currentSlide = this.Reveal.getCurrentSlide();
|
||||
if( currentSlide && this.Reveal.getConfig().fragments ) {
|
||||
if( slide && this.Reveal.getConfig().fragments ) {
|
||||
|
||||
fragments = fragments || this.sort( currentSlide.querySelectorAll( '.fragment' ) );
|
||||
fragments = fragments || this.sort( slide.querySelectorAll( '.fragment' ) );
|
||||
|
||||
if( fragments.length ) {
|
||||
|
||||
let maxIndex = 0;
|
||||
|
||||
if( typeof index !== 'number' ) {
|
||||
let currentFragment = this.sort( currentSlide.querySelectorAll( '.fragment.visible' ) ).pop();
|
||||
let currentFragment = this.sort( slide.querySelectorAll( '.fragment.visible' ) ).pop();
|
||||
if( currentFragment ) {
|
||||
index = parseInt( currentFragment.getAttribute( 'data-fragment-index' ) || 0, 10 );
|
||||
}
|
||||
@@ -252,12 +251,32 @@ export default class Fragments {
|
||||
// the current fragment index.
|
||||
index = typeof index === 'number' ? index : -1;
|
||||
index = Math.max( Math.min( index, maxIndex ), -1 );
|
||||
currentSlide.setAttribute( 'data-fragment', index );
|
||||
slide.setAttribute( 'data-fragment', index );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if( changedFragments.hidden.length ) {
|
||||
this.Reveal.dispatchEvent({
|
||||
type: 'fragmenthidden',
|
||||
data: {
|
||||
fragment: changedFragments.hidden[0],
|
||||
fragments: changedFragments.hidden
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if( changedFragments.shown.length ) {
|
||||
this.Reveal.dispatchEvent({
|
||||
type: 'fragmentshown',
|
||||
data: {
|
||||
fragment: changedFragments.shown[0],
|
||||
fragments: changedFragments.shown
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return changedFragments;
|
||||
|
||||
}
|
||||
@@ -312,26 +331,6 @@ export default class Fragments {
|
||||
|
||||
let changedFragments = this.update( index, fragments );
|
||||
|
||||
if( changedFragments.hidden.length ) {
|
||||
this.Reveal.dispatchEvent({
|
||||
type: 'fragmenthidden',
|
||||
data: {
|
||||
fragment: changedFragments.hidden[0],
|
||||
fragments: changedFragments.hidden
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if( changedFragments.shown.length ) {
|
||||
this.Reveal.dispatchEvent({
|
||||
type: 'fragmentshown',
|
||||
data: {
|
||||
fragment: changedFragments.shown[0],
|
||||
fragments: changedFragments.shown
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.Reveal.controls.update();
|
||||
this.Reveal.progress.update();
|
||||
|
||||
|
||||
197
scripts/reveal.js/js/controllers/jumptoslide.js
Normal file
197
scripts/reveal.js/js/controllers/jumptoslide.js
Normal file
@@ -0,0 +1,197 @@
|
||||
import {
|
||||
SLIDE_NUMBER_FORMAT_CURRENT,
|
||||
SLIDE_NUMBER_FORMAT_CURRENT_SLASH_TOTAL
|
||||
} from "../utils/constants";
|
||||
|
||||
/**
|
||||
* Makes it possible to jump to a slide by entering its
|
||||
* slide number or id.
|
||||
*/
|
||||
export default class JumpToSlide {
|
||||
|
||||
constructor( Reveal ) {
|
||||
|
||||
this.Reveal = Reveal;
|
||||
|
||||
this.onInput = this.onInput.bind( this );
|
||||
this.onBlur = this.onBlur.bind( this );
|
||||
this.onKeyDown = this.onKeyDown.bind( this );
|
||||
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
this.element = document.createElement( 'div' );
|
||||
this.element.className = 'jump-to-slide';
|
||||
|
||||
this.jumpInput = document.createElement( 'input' );
|
||||
this.jumpInput.type = 'text';
|
||||
this.jumpInput.className = 'jump-to-slide-input';
|
||||
this.jumpInput.placeholder = 'Jump to slide';
|
||||
this.jumpInput.addEventListener( 'input', this.onInput );
|
||||
this.jumpInput.addEventListener( 'keydown', this.onKeyDown );
|
||||
this.jumpInput.addEventListener( 'blur', this.onBlur );
|
||||
|
||||
this.element.appendChild( this.jumpInput );
|
||||
|
||||
}
|
||||
|
||||
show() {
|
||||
|
||||
this.indicesOnShow = this.Reveal.getIndices();
|
||||
|
||||
this.Reveal.getRevealElement().appendChild( this.element );
|
||||
this.jumpInput.focus();
|
||||
|
||||
}
|
||||
|
||||
hide() {
|
||||
|
||||
if( this.isVisible() ) {
|
||||
this.element.remove();
|
||||
this.jumpInput.value = '';
|
||||
|
||||
clearTimeout( this.jumpTimeout );
|
||||
delete this.jumpTimeout;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
isVisible() {
|
||||
|
||||
return !!this.element.parentNode;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the current input and jumps to the given slide.
|
||||
*/
|
||||
jump() {
|
||||
|
||||
clearTimeout( this.jumpTimeout );
|
||||
delete this.jumpTimeout;
|
||||
|
||||
let query = this.jumpInput.value.trim( '' );
|
||||
let indices;
|
||||
|
||||
// When slide numbers are formatted to be a single linear number
|
||||
// (instead of showing a separate horizontal/vertical index) we
|
||||
// use the same format for slide jumps
|
||||
if( /^\d+$/.test( query ) ) {
|
||||
const slideNumberFormat = this.Reveal.getConfig().slideNumber;
|
||||
if( slideNumberFormat === SLIDE_NUMBER_FORMAT_CURRENT || slideNumberFormat === SLIDE_NUMBER_FORMAT_CURRENT_SLASH_TOTAL ) {
|
||||
const slide = this.Reveal.getSlides()[ parseInt( query, 10 ) - 1 ];
|
||||
if( slide ) {
|
||||
indices = this.Reveal.getIndices( slide );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if( !indices ) {
|
||||
// If the query uses "horizontal.vertical" format, convert to
|
||||
// "horizontal/vertical" so that our URL parser can understand
|
||||
if( /^\d+\.\d+$/.test( query ) ) {
|
||||
query = query.replace( '.', '/' );
|
||||
}
|
||||
|
||||
indices = this.Reveal.location.getIndicesFromHash( query, { oneBasedIndex: true } );
|
||||
}
|
||||
|
||||
// Still no valid index? Fall back on a text search
|
||||
if( !indices && /\S+/i.test( query ) && query.length > 1 ) {
|
||||
indices = this.search( query );
|
||||
}
|
||||
|
||||
if( indices && query !== '' ) {
|
||||
this.Reveal.slide( indices.h, indices.v, indices.f );
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
this.Reveal.slide( this.indicesOnShow.h, this.indicesOnShow.v, this.indicesOnShow.f );
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
jumpAfter( delay ) {
|
||||
|
||||
clearTimeout( this.jumpTimeout );
|
||||
this.jumpTimeout = setTimeout( () => this.jump(), delay );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A lofi search that looks for the given query in all
|
||||
* of our slides and returns the first match.
|
||||
*/
|
||||
search( query ) {
|
||||
|
||||
const regex = new RegExp( '\\b' + query.trim() + '\\b', 'i' );
|
||||
|
||||
const slide = this.Reveal.getSlides().find( ( slide ) => {
|
||||
return regex.test( slide.innerText );
|
||||
} );
|
||||
|
||||
if( slide ) {
|
||||
return this.Reveal.getIndices( slide );
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverts back to the slide we were on when jump to slide was
|
||||
* invoked.
|
||||
*/
|
||||
cancel() {
|
||||
|
||||
this.Reveal.slide( this.indicesOnShow.h, this.indicesOnShow.v, this.indicesOnShow.f );
|
||||
this.hide();
|
||||
|
||||
}
|
||||
|
||||
confirm() {
|
||||
|
||||
this.jump();
|
||||
this.hide();
|
||||
|
||||
}
|
||||
|
||||
destroy() {
|
||||
|
||||
this.jumpInput.removeEventListener( 'input', this.onInput );
|
||||
this.jumpInput.removeEventListener( 'keydown', this.onKeyDown );
|
||||
this.jumpInput.removeEventListener( 'blur', this.onBlur );
|
||||
|
||||
this.element.remove();
|
||||
|
||||
}
|
||||
|
||||
onKeyDown( event ) {
|
||||
|
||||
if( event.keyCode === 13 ) {
|
||||
this.confirm();
|
||||
}
|
||||
else if( event.keyCode === 27 ) {
|
||||
this.cancel();
|
||||
|
||||
event.stopImmediatePropagation();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
onInput( event ) {
|
||||
|
||||
this.jumpAfter( 200 );
|
||||
|
||||
}
|
||||
|
||||
onBlur() {
|
||||
|
||||
setTimeout( () => this.hide(), 1 );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -17,7 +17,6 @@ export default class Keyboard {
|
||||
this.bindings = {};
|
||||
|
||||
this.onDocumentKeyDown = this.onDocumentKeyDown.bind( this );
|
||||
this.onDocumentKeyPress = this.onDocumentKeyPress.bind( this );
|
||||
|
||||
}
|
||||
|
||||
@@ -43,6 +42,7 @@ export default class Keyboard {
|
||||
this.shortcuts['Shift + ←/↑/→/↓'] = 'Jump to first/last slide';
|
||||
this.shortcuts['B , .'] = 'Pause';
|
||||
this.shortcuts['F'] = 'Fullscreen';
|
||||
this.shortcuts['G'] = 'Jump to slide';
|
||||
this.shortcuts['ESC, O'] = 'Slide overview';
|
||||
|
||||
}
|
||||
@@ -53,7 +53,6 @@ export default class Keyboard {
|
||||
bind() {
|
||||
|
||||
document.addEventListener( 'keydown', this.onDocumentKeyDown, false );
|
||||
document.addEventListener( 'keypress', this.onDocumentKeyPress, false );
|
||||
|
||||
}
|
||||
|
||||
@@ -63,7 +62,6 @@ export default class Keyboard {
|
||||
unbind() {
|
||||
|
||||
document.removeEventListener( 'keydown', this.onDocumentKeyDown, false );
|
||||
document.removeEventListener( 'keypress', this.onDocumentKeyPress, false );
|
||||
|
||||
}
|
||||
|
||||
@@ -134,20 +132,6 @@ export default class Keyboard {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for the document level 'keypress' event.
|
||||
*
|
||||
* @param {object} event
|
||||
*/
|
||||
onDocumentKeyPress( event ) {
|
||||
|
||||
// Check if the pressed key is question mark
|
||||
if( event.shiftKey && event.charCode === 63 ) {
|
||||
this.Reveal.toggleHelp();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for the document level 'keydown' event.
|
||||
*
|
||||
@@ -183,10 +167,10 @@ export default class Keyboard {
|
||||
let activeElementIsNotes = document.activeElement && document.activeElement.className && /speaker-notes/i.test( document.activeElement.className);
|
||||
|
||||
// Whitelist certain modifiers for slide navigation shortcuts
|
||||
let isNavigationKey = [32, 37, 38, 39, 40, 78, 80].indexOf( event.keyCode ) !== -1;
|
||||
let keyCodeUsesModifier = [32, 37, 38, 39, 40, 63, 78, 80, 191].indexOf( event.keyCode ) !== -1;
|
||||
|
||||
// Prevent all other events when a modifier is pressed
|
||||
let unusedModifier = !( isNavigationKey && event.shiftKey || event.altKey ) &&
|
||||
let unusedModifier = !( keyCodeUsesModifier && event.shiftKey || event.altKey ) &&
|
||||
( event.shiftKey || event.altKey || event.ctrlKey || event.metaKey );
|
||||
|
||||
// Disregard the event if there's a focused element or a
|
||||
@@ -194,7 +178,7 @@ export default class Keyboard {
|
||||
if( activeElementIsCE || activeElementIsInput || activeElementIsNotes || unusedModifier ) return;
|
||||
|
||||
// While paused only allow resume keyboard events; 'b', 'v', '.'
|
||||
let resumeKeyCodes = [66,86,190,191];
|
||||
let resumeKeyCodes = [66,86,190,191,112];
|
||||
let key;
|
||||
|
||||
// Custom key bindings for togglePause should be able to resume
|
||||
@@ -206,6 +190,10 @@ export default class Keyboard {
|
||||
}
|
||||
}
|
||||
|
||||
if( this.Reveal.isOverlayOpen() && !["Escape", "f", "c", "b", "."].includes(event.key) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if( this.Reveal.isPaused() && resumeKeyCodes.indexOf( keyCode ) === -1 ) {
|
||||
return false;
|
||||
}
|
||||
@@ -287,7 +275,12 @@ export default class Keyboard {
|
||||
this.Reveal.slide( 0 );
|
||||
}
|
||||
else if( !this.Reveal.overview.isActive() && useLinearMode ) {
|
||||
this.Reveal.prev({skipFragments: event.altKey});
|
||||
if( config.rtl ) {
|
||||
this.Reveal.next({skipFragments: event.altKey});
|
||||
}
|
||||
else {
|
||||
this.Reveal.prev({skipFragments: event.altKey});
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.Reveal.left({skipFragments: event.altKey});
|
||||
@@ -299,7 +292,12 @@ export default class Keyboard {
|
||||
this.Reveal.slide( this.Reveal.getHorizontalSlides().length - 1 );
|
||||
}
|
||||
else if( !this.Reveal.overview.isActive() && useLinearMode ) {
|
||||
this.Reveal.next({skipFragments: event.altKey});
|
||||
if( config.rtl ) {
|
||||
this.Reveal.prev({skipFragments: event.altKey});
|
||||
}
|
||||
else {
|
||||
this.Reveal.next({skipFragments: event.altKey});
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.Reveal.right({skipFragments: event.altKey});
|
||||
@@ -350,7 +348,7 @@ export default class Keyboard {
|
||||
}
|
||||
}
|
||||
// TWO-SPOT, SEMICOLON, B, V, PERIOD, LOGITECH PRESENTER TOOLS "BLACK SCREEN" BUTTON
|
||||
else if( keyCode === 58 || keyCode === 59 || keyCode === 66 || keyCode === 86 || keyCode === 190 || keyCode === 191 ) {
|
||||
else if( [58, 59, 66, 86, 190].includes( keyCode ) || ( keyCode === 191 && !event.shiftKey ) ) {
|
||||
this.Reveal.togglePause();
|
||||
}
|
||||
// F
|
||||
@@ -359,10 +357,28 @@ export default class Keyboard {
|
||||
}
|
||||
// A
|
||||
else if( keyCode === 65 ) {
|
||||
if ( config.autoSlideStoppable ) {
|
||||
if( config.autoSlideStoppable ) {
|
||||
this.Reveal.toggleAutoSlide( autoSlideWasPaused );
|
||||
}
|
||||
}
|
||||
// G
|
||||
else if( keyCode === 71 ) {
|
||||
if( config.jumpToSlide ) {
|
||||
this.Reveal.toggleJumpToSlide();
|
||||
}
|
||||
}
|
||||
// C
|
||||
else if( keyCode === 67 && this.Reveal.isOverlayOpen() ) {
|
||||
this.Reveal.closeOverlay();
|
||||
}
|
||||
// ?
|
||||
else if( ( keyCode === 63 || keyCode === 191 ) && event.shiftKey ) {
|
||||
this.Reveal.toggleHelp();
|
||||
}
|
||||
// F1
|
||||
else if( keyCode === 112 ) {
|
||||
this.Reveal.toggleHelp();
|
||||
}
|
||||
else {
|
||||
triggered = false;
|
||||
}
|
||||
@@ -389,4 +405,4 @@ export default class Keyboard {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ export default class Location {
|
||||
*
|
||||
* @returns slide indices or null
|
||||
*/
|
||||
getIndicesFromHash( hash=window.location.hash ) {
|
||||
getIndicesFromHash( hash=window.location.hash, options={} ) {
|
||||
|
||||
// Attempt to parse the hash as either an index or name
|
||||
let name = hash.replace( /^#\/?/, '' );
|
||||
@@ -49,7 +49,7 @@ export default class Location {
|
||||
// If the first bit is not fully numeric and there is a name we
|
||||
// can assume that this is a named link
|
||||
if( !/^[0-9]*$/.test( bits[0] ) && name.length ) {
|
||||
let element;
|
||||
let slide;
|
||||
|
||||
let f;
|
||||
|
||||
@@ -62,17 +62,19 @@ export default class Location {
|
||||
|
||||
// Ensure the named link is a valid HTML ID attribute
|
||||
try {
|
||||
element = document.getElementById( decodeURIComponent( name ) );
|
||||
slide = document
|
||||
.getElementById( decodeURIComponent( name ) )
|
||||
.closest('.slides section');
|
||||
}
|
||||
catch ( error ) { }
|
||||
|
||||
if( element ) {
|
||||
return { ...this.Reveal.getIndices( element ), f };
|
||||
if( slide ) {
|
||||
return { ...this.Reveal.getIndices( slide ), f };
|
||||
}
|
||||
}
|
||||
else {
|
||||
const config = this.Reveal.getConfig();
|
||||
let hashIndexBase = config.hashOneBasedIndex ? 1 : 0;
|
||||
let hashIndexBase = config.hashOneBasedIndex || options.oneBasedIndex ? 1 : 0;
|
||||
|
||||
// Read the index components of the hash
|
||||
let h = ( parseInt( bits[0], 10 ) - hashIndexBase ) || 0,
|
||||
@@ -139,7 +141,7 @@ export default class Location {
|
||||
let hash = this.getHash();
|
||||
|
||||
// If we're configured to push to history OR the history
|
||||
// API is not avaialble.
|
||||
// API is not available.
|
||||
if( config.history ) {
|
||||
window.location.hash = hash;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Handles the showing and
|
||||
* Handles the showing of speaker notes
|
||||
*/
|
||||
export default class Notes {
|
||||
|
||||
@@ -38,10 +38,12 @@ export default class Notes {
|
||||
*/
|
||||
update() {
|
||||
|
||||
if( this.Reveal.getConfig().showNotes && this.element && this.Reveal.getCurrentSlide() && !this.Reveal.print.isPrintingPDF() ) {
|
||||
|
||||
if( this.Reveal.getConfig().showNotes &&
|
||||
this.element && this.Reveal.getCurrentSlide() &&
|
||||
!this.Reveal.isScrollView() &&
|
||||
!this.Reveal.isPrintView()
|
||||
) {
|
||||
this.element.innerHTML = this.getSlideNotes() || '<span class="notes-placeholder">No notes on this slide.</span>';
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -54,7 +56,11 @@ export default class Notes {
|
||||
*/
|
||||
updateVisibility() {
|
||||
|
||||
if( this.Reveal.getConfig().showNotes && this.hasNotes() && !this.Reveal.print.isPrintingPDF() ) {
|
||||
if( this.Reveal.getConfig().showNotes &&
|
||||
this.hasNotes() &&
|
||||
!this.Reveal.isScrollView() &&
|
||||
!this.Reveal.isPrintView()
|
||||
) {
|
||||
this.Reveal.getRevealElement().classList.add( 'show-notes' );
|
||||
}
|
||||
else {
|
||||
@@ -89,7 +95,7 @@ export default class Notes {
|
||||
* Retrieves the speaker notes from a slide. Notes can be
|
||||
* defined in two ways:
|
||||
* 1. As a data-notes attribute on the slide <section>
|
||||
* 2. As an <aside class="notes"> inside of the slide
|
||||
* 2. With <aside class="notes"> elements inside the slide
|
||||
*
|
||||
* @param {HTMLElement} [slide=currentSlide]
|
||||
* @return {(string|null)}
|
||||
@@ -101,10 +107,10 @@ export default class Notes {
|
||||
return slide.getAttribute( 'data-notes' );
|
||||
}
|
||||
|
||||
// ... or using an <aside class="notes"> element
|
||||
let notesElement = slide.querySelector( 'aside.notes' );
|
||||
if( notesElement ) {
|
||||
return notesElement.innerHTML;
|
||||
// ... or using <aside class="notes"> elements
|
||||
let notesElements = slide.querySelectorAll( 'aside.notes' );
|
||||
if( notesElements ) {
|
||||
return Array.from(notesElements).map( notesElement => notesElement.innerHTML ).join( '\n' );
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
389
scripts/reveal.js/js/controllers/overlay.js
Normal file
389
scripts/reveal.js/js/controllers/overlay.js
Normal file
@@ -0,0 +1,389 @@
|
||||
/**
|
||||
* Handles the display of reveal.js' overlay elements used
|
||||
* to preview iframes, images & videos.
|
||||
*/
|
||||
export default class Overlay {
|
||||
|
||||
constructor( Reveal ) {
|
||||
|
||||
this.Reveal = Reveal;
|
||||
|
||||
this.onSlidesClicked = this.onSlidesClicked.bind( this );
|
||||
|
||||
this.iframeTriggerSelector = null;
|
||||
this.mediaTriggerSelector = '[data-preview-image], [data-preview-video]';
|
||||
|
||||
this.stateProps = ['previewIframe', 'previewImage', 'previewVideo', 'previewFit'];
|
||||
this.state = {};
|
||||
|
||||
}
|
||||
|
||||
update() {
|
||||
|
||||
// Enable link previews globally
|
||||
if( this.Reveal.getConfig().previewLinks ) {
|
||||
this.iframeTriggerSelector = 'a[href]:not([data-preview-link=false]), [data-preview-link]:not(a):not([data-preview-link=false])';
|
||||
}
|
||||
// Enable link previews for individual elements
|
||||
else {
|
||||
this.iframeTriggerSelector = '[data-preview-link]:not([data-preview-link=false])';
|
||||
}
|
||||
|
||||
const hasLinkPreviews = this.Reveal.getSlidesElement().querySelectorAll( this.iframeTriggerSelector ).length > 0;
|
||||
const hasMediaPreviews = this.Reveal.getSlidesElement().querySelectorAll( this.mediaTriggerSelector ).length > 0;
|
||||
|
||||
// Only add the listener when there are previewable elements in the slides
|
||||
if( hasLinkPreviews || hasMediaPreviews ) {
|
||||
this.Reveal.getSlidesElement().addEventListener( 'click', this.onSlidesClicked, false );
|
||||
}
|
||||
else {
|
||||
this.Reveal.getSlidesElement().removeEventListener( 'click', this.onSlidesClicked, false );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
createOverlay( className ) {
|
||||
|
||||
this.dom = document.createElement( 'div' );
|
||||
this.dom.classList.add( 'r-overlay' );
|
||||
this.dom.classList.add( className );
|
||||
|
||||
this.viewport = document.createElement( 'div' );
|
||||
this.viewport.classList.add( 'r-overlay-viewport' );
|
||||
|
||||
this.dom.appendChild( this.viewport );
|
||||
this.Reveal.getRevealElement().appendChild( this.dom );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a lightbox that previews the target URL.
|
||||
*
|
||||
* @param {string} url - url for lightbox iframe src
|
||||
*/
|
||||
previewIframe( url ) {
|
||||
|
||||
this.close();
|
||||
|
||||
this.state = { previewIframe: url };
|
||||
|
||||
this.createOverlay( 'r-overlay-preview' );
|
||||
this.dom.dataset.state = 'loading';
|
||||
|
||||
this.viewport.innerHTML =
|
||||
`<header class="r-overlay-header">
|
||||
<a class="r-overlay-button r-overlay-external" href="${url}" target="_blank"><span class="icon"></span></a>
|
||||
<button class="r-overlay-button r-overlay-close"><span class="icon"></span></button>
|
||||
</header>
|
||||
<div class="r-overlay-spinner"></div>
|
||||
<div class="r-overlay-content">
|
||||
<iframe src="${url}"></iframe>
|
||||
<small class="r-overlay-content-inner">
|
||||
<span class="r-overlay-error x-frame-error">Unable to load iframe. This is likely due to the site's policy (x-frame-options).</span>
|
||||
</small>
|
||||
</div>`;
|
||||
|
||||
this.dom.querySelector( 'iframe' ).addEventListener( 'load', event => {
|
||||
this.dom.dataset.state = 'loaded';
|
||||
}, false );
|
||||
|
||||
this.dom.querySelector( '.r-overlay-close' ).addEventListener( 'click', event => {
|
||||
this.close();
|
||||
event.preventDefault();
|
||||
}, false );
|
||||
|
||||
this.dom.querySelector( '.r-overlay-external' ).addEventListener( 'click', event => {
|
||||
this.close();
|
||||
}, false );
|
||||
|
||||
this.Reveal.dispatchEvent({ type: 'previewiframe', data: { url } });
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a lightbox window that provides a larger view of the
|
||||
* given image/video.
|
||||
*
|
||||
* @param {string} url - url to the image/video to preview
|
||||
* @param {image|video} mediaType
|
||||
* @param {string} [fitMode] - the fit mode to use for the preview
|
||||
*/
|
||||
previewMedia( url, mediaType, fitMode ) {
|
||||
|
||||
if( mediaType !== 'image' && mediaType !== 'video' ) {
|
||||
console.warn( 'Please specify a valid media type to preview (image|video)' );
|
||||
return;
|
||||
}
|
||||
|
||||
this.close();
|
||||
|
||||
fitMode = fitMode || 'scale-down';
|
||||
|
||||
this.createOverlay( 'r-overlay-preview' );
|
||||
this.dom.dataset.state = 'loading';
|
||||
this.dom.dataset.previewFit = fitMode;
|
||||
|
||||
this.viewport.innerHTML =
|
||||
`<header class="r-overlay-header">
|
||||
<button class="r-overlay-button r-overlay-close">Esc <span class="icon"></span></button>
|
||||
</header>
|
||||
<div class="r-overlay-spinner"></div>
|
||||
<div class="r-overlay-content"></div>`;
|
||||
|
||||
const contentElement = this.dom.querySelector( '.r-overlay-content' );
|
||||
|
||||
if( mediaType === 'image' ) {
|
||||
|
||||
this.state = { previewImage: url, previewFit: fitMode }
|
||||
|
||||
const img = document.createElement( 'img', {} );
|
||||
img.src = url;
|
||||
contentElement.appendChild( img );
|
||||
|
||||
img.addEventListener( 'load', () => {
|
||||
this.dom.dataset.state = 'loaded';
|
||||
}, false );
|
||||
|
||||
img.addEventListener( 'error', () => {
|
||||
this.dom.dataset.state = 'error';
|
||||
contentElement.innerHTML =
|
||||
`<span class="r-overlay-error">Unable to load image.</span>`
|
||||
}, false );
|
||||
|
||||
// Hide image overlays when clicking outside the overlay
|
||||
this.dom.style.cursor = 'zoom-out';
|
||||
this.dom.addEventListener( 'click', ( event ) => {
|
||||
this.close();
|
||||
}, false );
|
||||
|
||||
this.Reveal.dispatchEvent({ type: 'previewimage', data: { url } });
|
||||
|
||||
}
|
||||
else if( mediaType === 'video' ) {
|
||||
|
||||
this.state = { previewVideo: url, previewFit: fitMode }
|
||||
|
||||
const video = document.createElement( 'video' );
|
||||
video.autoplay = this.dom.dataset.previewAutoplay === 'false' ? false : true;
|
||||
video.controls = this.dom.dataset.previewControls === 'false' ? false : true;
|
||||
video.loop = this.dom.dataset.previewLoop === 'true' ? true : false;
|
||||
video.muted = this.dom.dataset.previewMuted === 'true' ? true : false;
|
||||
video.playsInline = true;
|
||||
video.src = url;
|
||||
contentElement.appendChild( video );
|
||||
|
||||
video.addEventListener( 'loadeddata', () => {
|
||||
this.dom.dataset.state = 'loaded';
|
||||
}, false );
|
||||
|
||||
video.addEventListener( 'error', () => {
|
||||
this.dom.dataset.state = 'error';
|
||||
contentElement.innerHTML =
|
||||
`<span class="r-overlay-error">Unable to load video.</span>`;
|
||||
}, false );
|
||||
|
||||
this.Reveal.dispatchEvent({ type: 'previewvideo', data: { url } });
|
||||
|
||||
}
|
||||
else {
|
||||
throw new Error( 'Please specify a valid media type to preview' );
|
||||
}
|
||||
|
||||
this.dom.querySelector( '.r-overlay-close' ).addEventListener( 'click', ( event ) => {
|
||||
this.close();
|
||||
event.preventDefault();
|
||||
}, false );
|
||||
|
||||
}
|
||||
|
||||
previewImage( url, fitMode ) {
|
||||
|
||||
this.previewMedia( url, 'image', fitMode );
|
||||
|
||||
}
|
||||
|
||||
previewVideo( url, fitMode ) {
|
||||
|
||||
this.previewMedia( url, 'video', fitMode );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Open or close help overlay window.
|
||||
*
|
||||
* @param {Boolean} [override] Flag which overrides the
|
||||
* toggle logic and forcibly sets the desired state. True means
|
||||
* help is open, false means it's closed.
|
||||
*/
|
||||
toggleHelp( override ) {
|
||||
|
||||
if( typeof override === 'boolean' ) {
|
||||
override ? this.showHelp() : this.close();
|
||||
}
|
||||
else {
|
||||
if( this.dom ) {
|
||||
this.close();
|
||||
}
|
||||
else {
|
||||
this.showHelp();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens an overlay window with help material.
|
||||
*/
|
||||
showHelp() {
|
||||
|
||||
if( this.Reveal.getConfig().help ) {
|
||||
|
||||
this.close();
|
||||
|
||||
this.createOverlay( 'r-overlay-help' );
|
||||
|
||||
let html = '<p class="title">Keyboard Shortcuts</p>';
|
||||
|
||||
let shortcuts = this.Reveal.keyboard.getShortcuts(),
|
||||
bindings = this.Reveal.keyboard.getBindings();
|
||||
|
||||
html += '<table><th>KEY</th><th>ACTION</th>';
|
||||
for( let key in shortcuts ) {
|
||||
html += `<tr><td>${key}</td><td>${shortcuts[ key ]}</td></tr>`;
|
||||
}
|
||||
|
||||
// Add custom key bindings that have associated descriptions
|
||||
for( let binding in bindings ) {
|
||||
if( bindings[binding].key && bindings[binding].description ) {
|
||||
html += `<tr><td>${bindings[binding].key}</td><td>${bindings[binding].description}</td></tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
html += '</table>';
|
||||
|
||||
this.viewport.innerHTML = `
|
||||
<header class="r-overlay-header">
|
||||
<button class="r-overlay-button r-overlay-close">Esc <span class="icon"></span></button>
|
||||
</header>
|
||||
<div class="r-overlay-content">
|
||||
<div class="r-overlay-help-content">${html}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.dom.querySelector( '.r-overlay-close' ).addEventListener( 'click', event => {
|
||||
this.close();
|
||||
event.preventDefault();
|
||||
}, false );
|
||||
|
||||
this.Reveal.dispatchEvent({ type: 'showhelp' });
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
isOpen() {
|
||||
|
||||
return !!this.dom;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes any currently open overlay.
|
||||
*/
|
||||
close() {
|
||||
|
||||
if( this.dom ) {
|
||||
this.dom.remove();
|
||||
this.dom = null;
|
||||
|
||||
this.state = {};
|
||||
|
||||
this.Reveal.dispatchEvent({ type: 'closeoverlay' });
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
getState() {
|
||||
|
||||
return this.state;
|
||||
|
||||
}
|
||||
|
||||
setState( state ) {
|
||||
|
||||
// Ignore the incoming state if none of the preview related
|
||||
// props have changed
|
||||
if( this.stateProps.every( key => this.state[ key ] === state[ key ] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if( state.previewIframe ) {
|
||||
this.previewIframe( state.previewIframe );
|
||||
}
|
||||
else if( state.previewImage ) {
|
||||
this.previewImage( state.previewImage, state.previewFit );
|
||||
}
|
||||
else if( state.previewVideo ) {
|
||||
this.previewVideo( state.previewVideo, state.previewFit );
|
||||
}
|
||||
else {
|
||||
this.close();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
onSlidesClicked( event ) {
|
||||
|
||||
const target = event.target;
|
||||
|
||||
const linkTarget = target.closest( this.iframeTriggerSelector );
|
||||
const mediaTarget = target.closest( this.mediaTriggerSelector );
|
||||
|
||||
// Was an iframe lightbox trigger clicked?
|
||||
if( linkTarget ) {
|
||||
if( event.metaKey || event.shiftKey || event.altKey ) {
|
||||
// Let the browser handle meta keys naturally so users can cmd+click
|
||||
return;
|
||||
}
|
||||
let url = linkTarget.getAttribute( 'href' ) || linkTarget.getAttribute( 'data-preview-link' );
|
||||
if( url ) {
|
||||
this.previewIframe( url );
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
// Was a media lightbox trigger clicked?
|
||||
else if( mediaTarget ) {
|
||||
if( mediaTarget.hasAttribute( 'data-preview-image' ) ) {
|
||||
let url = mediaTarget.dataset.previewImage || mediaTarget.getAttribute( 'src' );
|
||||
if( url ) {
|
||||
this.previewImage( url, mediaTarget.dataset.previewFit );
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
else if( mediaTarget.hasAttribute( 'data-preview-video' ) ) {
|
||||
let url = mediaTarget.dataset.previewVideo || mediaTarget.getAttribute( 'src' );
|
||||
if( !url ) {
|
||||
let source = mediaTarget.querySelector( 'source' );
|
||||
if( source ) {
|
||||
url = source.getAttribute( 'src' );
|
||||
}
|
||||
}
|
||||
if( url ) {
|
||||
this.previewVideo( url, mediaTarget.dataset.previewFit );
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
destroy() {
|
||||
|
||||
this.close();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -24,7 +24,7 @@ export default class Overview {
|
||||
activate() {
|
||||
|
||||
// Only proceed if enabled in config
|
||||
if( this.Reveal.getConfig().overview && !this.isActive() ) {
|
||||
if( this.Reveal.getConfig().overview && !this.Reveal.isScrollView() && !this.isActive() ) {
|
||||
|
||||
this.active = true;
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ export default class Plugins {
|
||||
// Flags our current state (idle -> loading -> loaded)
|
||||
this.state = 'idle';
|
||||
|
||||
// An id:instance map of currently registed plugins
|
||||
// An id:instance map of currently registered plugins
|
||||
this.registeredPlugins = {};
|
||||
|
||||
this.asyncDependencies = [];
|
||||
@@ -171,7 +171,7 @@ export default class Plugins {
|
||||
/**
|
||||
* Registers a new plugin with this reveal.js instance.
|
||||
*
|
||||
* reveal.js waits for all regisered plugins to initialize
|
||||
* reveal.js waits for all registered plugins to initialize
|
||||
* before considering itself ready, as long as the plugin
|
||||
* is registered before calling `Reveal.initialize()`.
|
||||
*/
|
||||
|
||||
@@ -27,12 +27,10 @@ export default class Pointer {
|
||||
configure( config, oldConfig ) {
|
||||
|
||||
if( config.mouseWheel ) {
|
||||
document.addEventListener( 'DOMMouseScroll', this.onDocumentMouseScroll, false ); // FF
|
||||
document.addEventListener( 'mousewheel', this.onDocumentMouseScroll, false );
|
||||
document.addEventListener( 'wheel', this.onDocumentMouseScroll, false );
|
||||
}
|
||||
else {
|
||||
document.removeEventListener( 'DOMMouseScroll', this.onDocumentMouseScroll, false ); // FF
|
||||
document.removeEventListener( 'mousewheel', this.onDocumentMouseScroll, false );
|
||||
document.removeEventListener( 'wheel', this.onDocumentMouseScroll, false );
|
||||
}
|
||||
|
||||
// Auto-hide the mouse pointer when its inactive
|
||||
@@ -79,8 +77,7 @@ export default class Pointer {
|
||||
|
||||
this.showCursor();
|
||||
|
||||
document.removeEventListener( 'DOMMouseScroll', this.onDocumentMouseScroll, false );
|
||||
document.removeEventListener( 'mousewheel', this.onDocumentMouseScroll, false );
|
||||
document.removeEventListener( 'wheel', this.onDocumentMouseScroll, false );
|
||||
document.removeEventListener( 'mousemove', this.onDocumentCursorActive, false );
|
||||
document.removeEventListener( 'mousedown', this.onDocumentCursorActive, false );
|
||||
|
||||
@@ -126,4 +123,4 @@ export default class Pointer {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { queryAll, createStyleSheet } from '../utils/util.js'
|
||||
/**
|
||||
* Setups up our presentation for printing/exporting to PDF.
|
||||
*/
|
||||
export default class Print {
|
||||
export default class PrintView {
|
||||
|
||||
constructor( Reveal ) {
|
||||
|
||||
@@ -16,13 +16,13 @@ export default class Print {
|
||||
* Configures the presentation for printing to a static
|
||||
* PDF.
|
||||
*/
|
||||
async setupPDF() {
|
||||
async activate() {
|
||||
|
||||
const config = this.Reveal.getConfig();
|
||||
const slides = queryAll( this.Reveal.getRevealElement(), SLIDES_SELECTOR )
|
||||
|
||||
// Compute slide numbers now, before we start duplicating slides
|
||||
const doingSlideNumbers = config.slideNumber && /all|print/i.test( config.showSlideNumber );
|
||||
const injectPageNumbers = config.slideNumber && /all|print/i.test( config.showSlideNumber );
|
||||
|
||||
const slideSize = this.Reveal.getComputedSlideSize( window.innerWidth, window.innerHeight );
|
||||
|
||||
@@ -42,11 +42,11 @@ export default class Print {
|
||||
// Limit the size of certain elements to the dimensions of the slide
|
||||
createStyleSheet( '.reveal section>img, .reveal section>video, .reveal section>iframe{max-width: '+ slideWidth +'px; max-height:'+ slideHeight +'px}' );
|
||||
|
||||
document.documentElement.classList.add( 'print-pdf' );
|
||||
document.documentElement.classList.add( 'reveal-print', 'print-pdf' );
|
||||
document.body.style.width = pageWidth + 'px';
|
||||
document.body.style.height = pageHeight + 'px';
|
||||
|
||||
const viewportElement = document.querySelector( '.reveal-viewport' );
|
||||
const viewportElement = this.Reveal.getViewportElement();
|
||||
let presentationBackground;
|
||||
if( viewportElement ) {
|
||||
const viewportStyles = window.getComputedStyle( viewportElement );
|
||||
@@ -66,6 +66,7 @@ export default class Print {
|
||||
|
||||
const pages = [];
|
||||
const pageContainer = slides[0].parentNode;
|
||||
let slideNumber = 1;
|
||||
|
||||
// Slide and slide background layout
|
||||
slides.forEach( function( slide, index ) {
|
||||
@@ -109,9 +110,7 @@ export default class Print {
|
||||
slide.style.top = top + 'px';
|
||||
slide.style.width = slideWidth + 'px';
|
||||
|
||||
// Re-run the slide layout so that r-fit-text is applied based on
|
||||
// the printed slide size
|
||||
this.Reveal.slideContent.layout( slide )
|
||||
this.Reveal.slideContent.layout( slide );
|
||||
|
||||
if( slide.slideBackgroundElement ) {
|
||||
page.insertBefore( slide.slideBackgroundElement, slide );
|
||||
@@ -146,13 +145,12 @@ export default class Print {
|
||||
|
||||
}
|
||||
|
||||
// Inject slide numbers if `slideNumbers` are enabled
|
||||
if( doingSlideNumbers ) {
|
||||
const slideNumber = index + 1;
|
||||
// Inject page numbers if `slideNumbers` are enabled
|
||||
if( injectPageNumbers ) {
|
||||
const numberElement = document.createElement( 'div' );
|
||||
numberElement.classList.add( 'slide-number' );
|
||||
numberElement.classList.add( 'slide-number-pdf' );
|
||||
numberElement.innerHTML = slideNumber;
|
||||
numberElement.innerHTML = slideNumber++;
|
||||
page.appendChild( numberElement );
|
||||
}
|
||||
|
||||
@@ -166,7 +164,7 @@ export default class Print {
|
||||
|
||||
let previousFragmentStep;
|
||||
|
||||
fragmentGroups.forEach( function( fragments ) {
|
||||
fragmentGroups.forEach( function( fragments, index ) {
|
||||
|
||||
// Remove 'current-fragment' from the previous group
|
||||
if( previousFragmentStep ) {
|
||||
@@ -182,6 +180,14 @@ export default class Print {
|
||||
|
||||
// Create a separate page for the current fragment state
|
||||
const clonedPage = page.cloneNode( true );
|
||||
|
||||
// Inject unique page numbers for fragments
|
||||
if( injectPageNumbers ) {
|
||||
const numberElement = clonedPage.querySelector( '.slide-number-pdf' );
|
||||
const fragmentNumber = index + 1;
|
||||
numberElement.innerHTML += '.' + fragmentNumber;
|
||||
}
|
||||
|
||||
pages.push( clonedPage );
|
||||
|
||||
previousFragmentStep = fragments;
|
||||
@@ -211,18 +217,23 @@ export default class Print {
|
||||
|
||||
pages.forEach( page => pageContainer.appendChild( page ) );
|
||||
|
||||
// Re-run JS-based content layout after the slide is added to page DOM
|
||||
this.Reveal.slideContent.layout( this.Reveal.getSlidesElement() );
|
||||
|
||||
// Notify subscribers that the PDF layout is good to go
|
||||
this.Reveal.dispatchEvent({ type: 'pdf-ready' });
|
||||
|
||||
viewportElement.classList.remove( 'loading-scroll-mode' );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this instance is being used to print a PDF.
|
||||
* Checks if the print mode is/should be activated.
|
||||
*/
|
||||
isPrintingPDF() {
|
||||
isActive() {
|
||||
|
||||
return ( /print-pdf/gi ).test( window.location.search );
|
||||
return this.Reveal.getConfig().view === 'print';
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
923
scripts/reveal.js/js/controllers/scrollview.js
Normal file
923
scripts/reveal.js/js/controllers/scrollview.js
Normal file
@@ -0,0 +1,923 @@
|
||||
import { HORIZONTAL_SLIDES_SELECTOR, HORIZONTAL_BACKGROUNDS_SELECTOR } from '../utils/constants.js'
|
||||
import { queryAll } from '../utils/util.js'
|
||||
|
||||
const HIDE_SCROLLBAR_TIMEOUT = 500;
|
||||
const MAX_PROGRESS_SPACING = 4;
|
||||
const MIN_PROGRESS_SEGMENT_HEIGHT = 6;
|
||||
const MIN_PLAYHEAD_HEIGHT = 8;
|
||||
|
||||
/**
|
||||
* The scroll view lets you read a reveal.js presentation
|
||||
* as a linear scrollable page.
|
||||
*/
|
||||
export default class ScrollView {
|
||||
|
||||
constructor( Reveal ) {
|
||||
|
||||
this.Reveal = Reveal;
|
||||
|
||||
this.active = false;
|
||||
this.activatedCallbacks = [];
|
||||
|
||||
this.onScroll = this.onScroll.bind( this );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Activates the scroll view. This rearranges the presentation DOM
|
||||
* by—among other things—wrapping each slide in a page element.
|
||||
*/
|
||||
activate() {
|
||||
|
||||
if( this.active ) return;
|
||||
|
||||
const stateBeforeActivation = this.Reveal.getState();
|
||||
|
||||
this.active = true;
|
||||
|
||||
// Store the full presentation HTML so that we can restore it
|
||||
// when/if the scroll view is deactivated
|
||||
this.slideHTMLBeforeActivation = this.Reveal.getSlidesElement().innerHTML;
|
||||
|
||||
const horizontalSlides = queryAll( this.Reveal.getRevealElement(), HORIZONTAL_SLIDES_SELECTOR );
|
||||
const horizontalBackgrounds = queryAll( this.Reveal.getRevealElement(), HORIZONTAL_BACKGROUNDS_SELECTOR );
|
||||
|
||||
this.viewportElement.classList.add( 'loading-scroll-mode', 'reveal-scroll' );
|
||||
|
||||
let presentationBackground;
|
||||
|
||||
const viewportStyles = window.getComputedStyle( this.viewportElement );
|
||||
if( viewportStyles && viewportStyles.background ) {
|
||||
presentationBackground = viewportStyles.background;
|
||||
}
|
||||
|
||||
const pageElements = [];
|
||||
const pageContainer = horizontalSlides[0].parentNode;
|
||||
|
||||
let previousSlide;
|
||||
|
||||
// Creates a new page element and appends the given slide/bg
|
||||
// to it.
|
||||
const createPageElement = ( slide, h, v, isVertical ) => {
|
||||
|
||||
let contentContainer;
|
||||
|
||||
// If this slide is part of an auto-animation sequence, we
|
||||
// group it under the same page element as the previous slide
|
||||
if( previousSlide && this.Reveal.shouldAutoAnimateBetween( previousSlide, slide ) ) {
|
||||
contentContainer = document.createElement( 'div' );
|
||||
contentContainer.className = 'scroll-page-content scroll-auto-animate-page';
|
||||
contentContainer.style.display = 'none';
|
||||
previousSlide.closest( '.scroll-page-content' ).parentNode.appendChild( contentContainer );
|
||||
}
|
||||
else {
|
||||
// Wrap the slide in a page element and hide its overflow
|
||||
// so that no page ever flows onto another
|
||||
const page = document.createElement( 'div' );
|
||||
page.className = 'scroll-page';
|
||||
pageElements.push( page );
|
||||
|
||||
// This transfers over the background of the vertical stack containing
|
||||
// the slide if it exists. Otherwise, it uses the presentation-wide
|
||||
// background.
|
||||
if( isVertical && horizontalBackgrounds.length > h ) {
|
||||
const slideBackground = horizontalBackgrounds[h];
|
||||
const pageBackground = window.getComputedStyle( slideBackground );
|
||||
|
||||
if( pageBackground && pageBackground.background ) {
|
||||
page.style.background = pageBackground.background;
|
||||
}
|
||||
else if( presentationBackground ) {
|
||||
page.style.background = presentationBackground;
|
||||
}
|
||||
} else if( presentationBackground ) {
|
||||
page.style.background = presentationBackground;
|
||||
}
|
||||
|
||||
const stickyContainer = document.createElement( 'div' );
|
||||
stickyContainer.className = 'scroll-page-sticky';
|
||||
page.appendChild( stickyContainer );
|
||||
|
||||
contentContainer = document.createElement( 'div' );
|
||||
contentContainer.className = 'scroll-page-content';
|
||||
stickyContainer.appendChild( contentContainer );
|
||||
}
|
||||
|
||||
contentContainer.appendChild( slide );
|
||||
|
||||
slide.classList.remove( 'past', 'future' );
|
||||
slide.setAttribute( 'data-index-h', h );
|
||||
slide.setAttribute( 'data-index-v', v );
|
||||
|
||||
if( slide.slideBackgroundElement ) {
|
||||
slide.slideBackgroundElement.remove( 'past', 'future' );
|
||||
contentContainer.insertBefore( slide.slideBackgroundElement, slide );
|
||||
}
|
||||
|
||||
previousSlide = slide;
|
||||
|
||||
}
|
||||
|
||||
// Slide and slide background layout
|
||||
horizontalSlides.forEach( ( horizontalSlide, h ) => {
|
||||
|
||||
if( this.Reveal.isVerticalStack( horizontalSlide ) ) {
|
||||
horizontalSlide.querySelectorAll( 'section' ).forEach( ( verticalSlide, v ) => {
|
||||
createPageElement( verticalSlide, h, v, true );
|
||||
});
|
||||
}
|
||||
else {
|
||||
createPageElement( horizontalSlide, h, 0 );
|
||||
}
|
||||
|
||||
}, this );
|
||||
|
||||
this.createProgressBar();
|
||||
|
||||
// Remove leftover stacks
|
||||
queryAll( this.Reveal.getRevealElement(), '.stack' ).forEach( stack => stack.remove() );
|
||||
|
||||
// Add our newly created pages to the DOM
|
||||
pageElements.forEach( page => pageContainer.appendChild( page ) );
|
||||
|
||||
// Re-run JS-based content layout after the slide is added to page DOM
|
||||
this.Reveal.slideContent.layout( this.Reveal.getSlidesElement() );
|
||||
|
||||
this.Reveal.layout();
|
||||
this.Reveal.setState( stateBeforeActivation );
|
||||
|
||||
this.activatedCallbacks.forEach( callback => callback() );
|
||||
this.activatedCallbacks = [];
|
||||
|
||||
this.restoreScrollPosition();
|
||||
|
||||
this.viewportElement.classList.remove( 'loading-scroll-mode' );
|
||||
this.viewportElement.addEventListener( 'scroll', this.onScroll, { passive: true } );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Deactivates the scroll view and restores the standard slide-based
|
||||
* presentation.
|
||||
*/
|
||||
deactivate() {
|
||||
|
||||
if( !this.active ) return;
|
||||
|
||||
const stateBeforeDeactivation = this.Reveal.getState();
|
||||
|
||||
this.active = false;
|
||||
|
||||
this.viewportElement.removeEventListener( 'scroll', this.onScroll );
|
||||
this.viewportElement.classList.remove( 'reveal-scroll' );
|
||||
|
||||
this.removeProgressBar();
|
||||
|
||||
this.Reveal.getSlidesElement().innerHTML = this.slideHTMLBeforeActivation;
|
||||
this.Reveal.sync();
|
||||
this.Reveal.setState( stateBeforeDeactivation );
|
||||
|
||||
this.slideHTMLBeforeActivation = null;
|
||||
|
||||
}
|
||||
|
||||
toggle( override ) {
|
||||
|
||||
if( typeof override === 'boolean' ) {
|
||||
override ? this.activate() : this.deactivate();
|
||||
}
|
||||
else {
|
||||
this.isActive() ? this.deactivate() : this.activate();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the scroll view is currently active.
|
||||
*/
|
||||
isActive() {
|
||||
|
||||
return this.active;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the progress bar component.
|
||||
*/
|
||||
createProgressBar() {
|
||||
|
||||
this.progressBar = document.createElement( 'div' );
|
||||
this.progressBar.className = 'scrollbar';
|
||||
|
||||
this.progressBarInner = document.createElement( 'div' );
|
||||
this.progressBarInner.className = 'scrollbar-inner';
|
||||
this.progressBar.appendChild( this.progressBarInner );
|
||||
|
||||
this.progressBarPlayhead = document.createElement( 'div' );
|
||||
this.progressBarPlayhead.className = 'scrollbar-playhead';
|
||||
this.progressBarInner.appendChild( this.progressBarPlayhead );
|
||||
|
||||
this.viewportElement.insertBefore( this.progressBar, this.viewportElement.firstChild );
|
||||
|
||||
const handleDocumentMouseMove = ( event ) => {
|
||||
|
||||
let progress = ( event.clientY - this.progressBarInner.getBoundingClientRect().top ) / this.progressBarHeight;
|
||||
progress = Math.max( Math.min( progress, 1 ), 0 );
|
||||
|
||||
this.viewportElement.scrollTop = progress * ( this.viewportElement.scrollHeight - this.viewportElement.offsetHeight );
|
||||
|
||||
};
|
||||
|
||||
const handleDocumentMouseUp = ( event ) => {
|
||||
|
||||
this.draggingProgressBar = false;
|
||||
this.showProgressBar();
|
||||
|
||||
document.removeEventListener( 'mousemove', handleDocumentMouseMove );
|
||||
document.removeEventListener( 'mouseup', handleDocumentMouseUp );
|
||||
|
||||
};
|
||||
|
||||
const handleMouseDown = ( event ) => {
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
this.draggingProgressBar = true;
|
||||
|
||||
document.addEventListener( 'mousemove', handleDocumentMouseMove );
|
||||
document.addEventListener( 'mouseup', handleDocumentMouseUp );
|
||||
|
||||
handleDocumentMouseMove( event );
|
||||
|
||||
};
|
||||
|
||||
this.progressBarInner.addEventListener( 'mousedown', handleMouseDown );
|
||||
|
||||
}
|
||||
|
||||
removeProgressBar() {
|
||||
|
||||
if( this.progressBar ) {
|
||||
this.progressBar.remove();
|
||||
this.progressBar = null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
layout() {
|
||||
|
||||
if( this.isActive() ) {
|
||||
this.syncPages();
|
||||
this.syncScrollPosition();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates our pages to match the latest configuration and
|
||||
* presentation size.
|
||||
*/
|
||||
syncPages() {
|
||||
|
||||
const config = this.Reveal.getConfig();
|
||||
|
||||
const slideSize = this.Reveal.getComputedSlideSize( window.innerWidth, window.innerHeight );
|
||||
const scale = this.Reveal.getScale();
|
||||
const useCompactLayout = config.scrollLayout === 'compact';
|
||||
|
||||
const viewportHeight = this.viewportElement.offsetHeight;
|
||||
const compactHeight = slideSize.height * scale;
|
||||
const pageHeight = useCompactLayout ? compactHeight : viewportHeight;
|
||||
|
||||
// The height that needs to be scrolled between scroll triggers
|
||||
this.scrollTriggerHeight = useCompactLayout ? compactHeight : viewportHeight;
|
||||
|
||||
this.viewportElement.style.setProperty( '--page-height', pageHeight + 'px' );
|
||||
this.viewportElement.style.scrollSnapType = typeof config.scrollSnap === 'string' ? `y ${config.scrollSnap}` : '';
|
||||
|
||||
// This will hold all scroll triggers used to show/hide slides
|
||||
this.slideTriggers = [];
|
||||
|
||||
const pageElements = Array.from( this.Reveal.getRevealElement().querySelectorAll( '.scroll-page' ) );
|
||||
|
||||
this.pages = pageElements.map( pageElement => {
|
||||
const page = this.createPage({
|
||||
pageElement,
|
||||
slideElement: pageElement.querySelector( 'section' ),
|
||||
stickyElement: pageElement.querySelector( '.scroll-page-sticky' ),
|
||||
contentElement: pageElement.querySelector( '.scroll-page-content' ),
|
||||
backgroundElement: pageElement.querySelector( '.slide-background' ),
|
||||
autoAnimateElements: pageElement.querySelectorAll( '.scroll-auto-animate-page' ),
|
||||
autoAnimatePages: []
|
||||
});
|
||||
|
||||
page.pageElement.style.setProperty( '--slide-height', config.center === true ? 'auto' : slideSize.height + 'px' );
|
||||
|
||||
this.slideTriggers.push({
|
||||
page: page,
|
||||
activate: () => this.activatePage( page ),
|
||||
deactivate: () => this.deactivatePage( page )
|
||||
});
|
||||
|
||||
// Create scroll triggers that show/hide fragments
|
||||
this.createFragmentTriggersForPage( page );
|
||||
|
||||
// Create scroll triggers for triggering auto-animate steps
|
||||
if( page.autoAnimateElements.length > 0 ) {
|
||||
this.createAutoAnimateTriggersForPage( page );
|
||||
}
|
||||
|
||||
let totalScrollTriggerCount = Math.max( page.scrollTriggers.length - 1, 0 );
|
||||
|
||||
// Each auto-animate step may include its own scroll triggers
|
||||
// for fragments, ensure we count those as well
|
||||
totalScrollTriggerCount += page.autoAnimatePages.reduce( ( total, page ) => {
|
||||
return total + Math.max( page.scrollTriggers.length - 1, 0 );
|
||||
}, page.autoAnimatePages.length );
|
||||
|
||||
// Clean up from previous renders
|
||||
page.pageElement.querySelectorAll( '.scroll-snap-point' ).forEach( el => el.remove() );
|
||||
|
||||
// Create snap points for all scroll triggers
|
||||
// - Can't be absolute in FF
|
||||
// - Can't be 0-height in Safari
|
||||
// - Can't use snap-align on parent in Safari because then
|
||||
// inner triggers won't work
|
||||
for( let i = 0; i < totalScrollTriggerCount + 1; i++ ) {
|
||||
const triggerStick = document.createElement( 'div' );
|
||||
triggerStick.className = 'scroll-snap-point';
|
||||
triggerStick.style.height = this.scrollTriggerHeight + 'px';
|
||||
triggerStick.style.scrollSnapAlign = useCompactLayout ? 'center' : 'start';
|
||||
page.pageElement.appendChild( triggerStick );
|
||||
|
||||
if( i === 0 ) {
|
||||
triggerStick.style.marginTop = -this.scrollTriggerHeight + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
// In the compact layout, only slides with scroll triggers cover the
|
||||
// full viewport height. This helps avoid empty gaps before or after
|
||||
// a sticky slide.
|
||||
if( useCompactLayout && page.scrollTriggers.length > 0 ) {
|
||||
page.pageHeight = viewportHeight;
|
||||
page.pageElement.style.setProperty( '--page-height', viewportHeight + 'px' );
|
||||
}
|
||||
else {
|
||||
page.pageHeight = pageHeight;
|
||||
page.pageElement.style.removeProperty( '--page-height' );
|
||||
}
|
||||
|
||||
// Add scroll padding based on how many scroll triggers we have
|
||||
page.scrollPadding = this.scrollTriggerHeight * totalScrollTriggerCount;
|
||||
|
||||
// The total height including scrollable space
|
||||
page.totalHeight = page.pageHeight + page.scrollPadding;
|
||||
|
||||
// This is used to pad the height of our page in CSS
|
||||
page.pageElement.style.setProperty( '--page-scroll-padding', page.scrollPadding + 'px' );
|
||||
|
||||
// If this is a sticky page, stick it to the vertical center
|
||||
if( totalScrollTriggerCount > 0 ) {
|
||||
page.stickyElement.style.position = 'sticky';
|
||||
page.stickyElement.style.top = Math.max( ( viewportHeight - page.pageHeight ) / 2, 0 ) + 'px';
|
||||
}
|
||||
else {
|
||||
page.stickyElement.style.position = 'relative';
|
||||
page.pageElement.style.scrollSnapAlign = page.pageHeight < viewportHeight ? 'center' : 'start';
|
||||
}
|
||||
|
||||
return page;
|
||||
} );
|
||||
|
||||
this.setTriggerRanges();
|
||||
|
||||
/*
|
||||
console.log(this.slideTriggers.map( t => {
|
||||
return {
|
||||
range: `${t.range[0].toFixed(2)}-${t.range[1].toFixed(2)}`,
|
||||
triggers: t.page.scrollTriggers.map( t => {
|
||||
return `${t.range[0].toFixed(2)}-${t.range[1].toFixed(2)}`
|
||||
}).join( ', ' ),
|
||||
}
|
||||
}))
|
||||
*/
|
||||
|
||||
this.viewportElement.setAttribute( 'data-scrollbar', config.scrollProgress );
|
||||
|
||||
if( config.scrollProgress && this.totalScrollTriggerCount > 1 ) {
|
||||
// Create the progress bar if it doesn't already exist
|
||||
if( !this.progressBar ) this.createProgressBar();
|
||||
|
||||
this.syncProgressBar();
|
||||
}
|
||||
else {
|
||||
this.removeProgressBar();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates and sets the scroll range for all of our scroll
|
||||
* triggers.
|
||||
*/
|
||||
setTriggerRanges() {
|
||||
|
||||
// Calculate the total number of scroll triggers
|
||||
this.totalScrollTriggerCount = this.slideTriggers.reduce( ( total, trigger ) => {
|
||||
return total + Math.max( trigger.page.scrollTriggers.length, 1 );
|
||||
}, 0 );
|
||||
|
||||
let rangeStart = 0;
|
||||
|
||||
// Calculate the scroll range of each scroll trigger on a scale
|
||||
// of 0-1
|
||||
this.slideTriggers.forEach( ( trigger, i ) => {
|
||||
trigger.range = [
|
||||
rangeStart,
|
||||
rangeStart + Math.max( trigger.page.scrollTriggers.length, 1 ) / this.totalScrollTriggerCount
|
||||
];
|
||||
|
||||
const scrollTriggerSegmentSize = ( trigger.range[1] - trigger.range[0] ) / trigger.page.scrollTriggers.length;
|
||||
// Set the range for each inner scroll trigger
|
||||
trigger.page.scrollTriggers.forEach( ( scrollTrigger, i ) => {
|
||||
scrollTrigger.range = [
|
||||
rangeStart + i * scrollTriggerSegmentSize,
|
||||
rangeStart + ( i + 1 ) * scrollTriggerSegmentSize
|
||||
];
|
||||
} );
|
||||
|
||||
rangeStart = trigger.range[1];
|
||||
} );
|
||||
|
||||
// Ensure the last trigger extends to the end of the page, otherwise
|
||||
// rounding errors can cause the last trigger to end at 0.999999...
|
||||
this.slideTriggers[this.slideTriggers.length - 1].range[1] = 1;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates one scroll trigger for each fragments in the given page.
|
||||
*
|
||||
* @param {*} page
|
||||
*/
|
||||
createFragmentTriggersForPage( page, slideElement ) {
|
||||
|
||||
slideElement = slideElement || page.slideElement;
|
||||
|
||||
// Each fragment 'group' is an array containing one or more
|
||||
// fragments. Multiple fragments that appear at the same time
|
||||
// are part of the same group.
|
||||
const fragmentGroups = this.Reveal.fragments.sort( slideElement.querySelectorAll( '.fragment' ), true );
|
||||
|
||||
// Create scroll triggers that show/hide fragments
|
||||
if( fragmentGroups.length ) {
|
||||
page.fragments = this.Reveal.fragments.sort( slideElement.querySelectorAll( '.fragment:not(.disabled)' ) );
|
||||
page.scrollTriggers.push(
|
||||
// Trigger for the initial state with no fragments visible
|
||||
{
|
||||
activate: () => {
|
||||
this.Reveal.fragments.update( -1, page.fragments, slideElement );
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Triggers for each fragment group
|
||||
fragmentGroups.forEach( ( fragments, i ) => {
|
||||
page.scrollTriggers.push({
|
||||
activate: () => {
|
||||
this.Reveal.fragments.update( i, page.fragments, slideElement );
|
||||
}
|
||||
});
|
||||
} );
|
||||
}
|
||||
|
||||
|
||||
return page.scrollTriggers.length;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates scroll triggers for the auto-animate steps in the
|
||||
* given page.
|
||||
*
|
||||
* @param {*} page
|
||||
*/
|
||||
createAutoAnimateTriggersForPage( page ) {
|
||||
|
||||
if( page.autoAnimateElements.length > 0 ) {
|
||||
|
||||
// Triggers for each subsequent auto-animate slide
|
||||
this.slideTriggers.push( ...Array.from( page.autoAnimateElements ).map( ( autoAnimateElement, i ) => {
|
||||
let autoAnimatePage = this.createPage({
|
||||
slideElement: autoAnimateElement.querySelector( 'section' ),
|
||||
contentElement: autoAnimateElement,
|
||||
backgroundElement: autoAnimateElement.querySelector( '.slide-background' )
|
||||
});
|
||||
|
||||
// Create fragment scroll triggers for the auto-animate slide
|
||||
this.createFragmentTriggersForPage( autoAnimatePage, autoAnimatePage.slideElement );
|
||||
|
||||
page.autoAnimatePages.push( autoAnimatePage );
|
||||
|
||||
// Return our slide trigger
|
||||
return {
|
||||
page: autoAnimatePage,
|
||||
activate: () => this.activatePage( autoAnimatePage ),
|
||||
deactivate: () => this.deactivatePage( autoAnimatePage )
|
||||
};
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for creating a page definition and adding
|
||||
* required fields. A "page" is a slide or auto-animate step.
|
||||
*/
|
||||
createPage( page ) {
|
||||
|
||||
page.scrollTriggers = [];
|
||||
page.indexh = parseInt( page.slideElement.getAttribute( 'data-index-h' ), 10 );
|
||||
page.indexv = parseInt( page.slideElement.getAttribute( 'data-index-v' ), 10 );
|
||||
|
||||
return page;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Rerenders progress bar segments so that they match the current
|
||||
* reveal.js config and size.
|
||||
*/
|
||||
syncProgressBar() {
|
||||
|
||||
this.progressBarInner.querySelectorAll( '.scrollbar-slide' ).forEach( slide => slide.remove() );
|
||||
|
||||
const scrollHeight = this.viewportElement.scrollHeight;
|
||||
const viewportHeight = this.viewportElement.offsetHeight;
|
||||
const viewportHeightFactor = viewportHeight / scrollHeight;
|
||||
|
||||
this.progressBarHeight = this.progressBarInner.offsetHeight;
|
||||
this.playheadHeight = Math.max( viewportHeightFactor * this.progressBarHeight, MIN_PLAYHEAD_HEIGHT );
|
||||
this.progressBarScrollableHeight = this.progressBarHeight - this.playheadHeight;
|
||||
|
||||
const progressSegmentHeight = viewportHeight / scrollHeight * this.progressBarHeight;
|
||||
const spacing = Math.min( progressSegmentHeight / 8, MAX_PROGRESS_SPACING );
|
||||
|
||||
this.progressBarPlayhead.style.height = this.playheadHeight - spacing + 'px';
|
||||
|
||||
// Don't show individual segments if they're too small
|
||||
if( progressSegmentHeight > MIN_PROGRESS_SEGMENT_HEIGHT ) {
|
||||
|
||||
this.slideTriggers.forEach( slideTrigger => {
|
||||
|
||||
const { page } = slideTrigger;
|
||||
|
||||
// Visual representation of a slide
|
||||
page.progressBarSlide = document.createElement( 'div' );
|
||||
page.progressBarSlide.className = 'scrollbar-slide';
|
||||
page.progressBarSlide.style.top = slideTrigger.range[0] * this.progressBarHeight + 'px';
|
||||
page.progressBarSlide.style.height = ( slideTrigger.range[1] - slideTrigger.range[0] ) * this.progressBarHeight - spacing + 'px';
|
||||
page.progressBarSlide.classList.toggle( 'has-triggers', page.scrollTriggers.length > 0 );
|
||||
this.progressBarInner.appendChild( page.progressBarSlide );
|
||||
|
||||
// Visual representations of each scroll trigger
|
||||
page.scrollTriggerElements = page.scrollTriggers.map( ( trigger, i ) => {
|
||||
|
||||
const triggerElement = document.createElement( 'div' );
|
||||
triggerElement.className = 'scrollbar-trigger';
|
||||
triggerElement.style.top = ( trigger.range[0] - slideTrigger.range[0] ) * this.progressBarHeight + 'px';
|
||||
triggerElement.style.height = ( trigger.range[1] - trigger.range[0] ) * this.progressBarHeight - spacing + 'px';
|
||||
page.progressBarSlide.appendChild( triggerElement );
|
||||
|
||||
if( i === 0 ) triggerElement.style.display = 'none';
|
||||
|
||||
return triggerElement;
|
||||
|
||||
} );
|
||||
|
||||
} );
|
||||
|
||||
}
|
||||
else {
|
||||
|
||||
this.pages.forEach( page => page.progressBarSlide = null );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the current scroll position and updates our active
|
||||
* trigger states accordingly.
|
||||
*/
|
||||
syncScrollPosition() {
|
||||
|
||||
const viewportHeight = this.viewportElement.offsetHeight;
|
||||
const viewportHeightFactor = viewportHeight / this.viewportElement.scrollHeight;
|
||||
|
||||
const scrollTop = this.viewportElement.scrollTop;
|
||||
const scrollHeight = this.viewportElement.scrollHeight - viewportHeight
|
||||
const scrollProgress = Math.max( Math.min( scrollTop / scrollHeight, 1 ), 0 );
|
||||
const scrollProgressMid = Math.max( Math.min( ( scrollTop + viewportHeight / 2 ) / this.viewportElement.scrollHeight, 1 ), 0 );
|
||||
|
||||
let activePage;
|
||||
|
||||
this.slideTriggers.forEach( ( trigger ) => {
|
||||
const { page } = trigger;
|
||||
|
||||
const shouldPreload = scrollProgress >= trigger.range[0] - viewportHeightFactor*2 &&
|
||||
scrollProgress <= trigger.range[1] + viewportHeightFactor*2;
|
||||
|
||||
// Load slides that are within the preload range
|
||||
if( shouldPreload && !page.loaded ) {
|
||||
page.loaded = true;
|
||||
this.Reveal.slideContent.load( page.slideElement );
|
||||
}
|
||||
else if( page.loaded ) {
|
||||
page.loaded = false;
|
||||
this.Reveal.slideContent.unload( page.slideElement );
|
||||
}
|
||||
|
||||
// If we're within this trigger range, activate it
|
||||
if( scrollProgress >= trigger.range[0] && scrollProgress <= trigger.range[1] ) {
|
||||
this.activateTrigger( trigger );
|
||||
activePage = trigger.page;
|
||||
}
|
||||
// .. otherwise deactivate
|
||||
else if( trigger.active ) {
|
||||
this.deactivateTrigger( trigger );
|
||||
}
|
||||
} );
|
||||
|
||||
// Each page can have its own scroll triggers, check if any of those
|
||||
// need to be activated/deactivated
|
||||
if( activePage ) {
|
||||
activePage.scrollTriggers.forEach( ( trigger ) => {
|
||||
if( scrollProgressMid >= trigger.range[0] && scrollProgressMid <= trigger.range[1] ) {
|
||||
this.activateTrigger( trigger );
|
||||
}
|
||||
else if( trigger.active ) {
|
||||
this.deactivateTrigger( trigger );
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
// Update our visual progress indication
|
||||
this.setProgressBarValue( scrollTop / ( this.viewportElement.scrollHeight - viewportHeight ) );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the progress bar playhead to the specified position.
|
||||
*
|
||||
* @param {number} progress 0-1
|
||||
*/
|
||||
setProgressBarValue( progress ) {
|
||||
|
||||
if( this.progressBar ) {
|
||||
|
||||
this.progressBarPlayhead.style.transform = `translateY(${progress * this.progressBarScrollableHeight}px)`;
|
||||
|
||||
this.getAllPages()
|
||||
.filter( page => page.progressBarSlide )
|
||||
.forEach( ( page ) => {
|
||||
page.progressBarSlide.classList.toggle( 'active', page.active === true );
|
||||
|
||||
page.scrollTriggers.forEach( ( trigger, i ) => {
|
||||
page.scrollTriggerElements[i].classList.toggle( 'active', page.active === true && trigger.active === true );
|
||||
} );
|
||||
} );
|
||||
|
||||
this.showProgressBar();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the progress bar and, if configured, automatically hide
|
||||
* it after a delay.
|
||||
*/
|
||||
showProgressBar() {
|
||||
|
||||
this.progressBar.classList.add( 'visible' );
|
||||
|
||||
clearTimeout( this.hideProgressBarTimeout );
|
||||
|
||||
if( this.Reveal.getConfig().scrollProgress === 'auto' && !this.draggingProgressBar ) {
|
||||
|
||||
this.hideProgressBarTimeout = setTimeout( () => {
|
||||
if( this.progressBar ) {
|
||||
this.progressBar.classList.remove( 'visible' );
|
||||
}
|
||||
}, HIDE_SCROLLBAR_TIMEOUT );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll to the previous page.
|
||||
*/
|
||||
prev() {
|
||||
|
||||
this.viewportElement.scrollTop -= this.scrollTriggerHeight;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll to the next page.
|
||||
*/
|
||||
next() {
|
||||
|
||||
this.viewportElement.scrollTop += this.scrollTriggerHeight;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrolls the given slide element into view.
|
||||
*
|
||||
* @param {HTMLElement} slideElement
|
||||
*/
|
||||
scrollToSlide( slideElement ) {
|
||||
|
||||
// If the scroll view isn't active yet, queue this action
|
||||
if( !this.active ) {
|
||||
this.activatedCallbacks.push( () => this.scrollToSlide( slideElement ) );
|
||||
}
|
||||
else {
|
||||
// Find the trigger for this slide
|
||||
const trigger = this.getScrollTriggerBySlide( slideElement );
|
||||
|
||||
if( trigger ) {
|
||||
// Use the trigger's range to calculate the scroll position
|
||||
this.viewportElement.scrollTop = trigger.range[0] * ( this.viewportElement.scrollHeight - this.viewportElement.offsetHeight );
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists the current scroll position to session storage
|
||||
* so that it can be restored.
|
||||
*/
|
||||
storeScrollPosition() {
|
||||
|
||||
clearTimeout( this.storeScrollPositionTimeout );
|
||||
|
||||
this.storeScrollPositionTimeout = setTimeout( () => {
|
||||
sessionStorage.setItem( 'reveal-scroll-top', this.viewportElement.scrollTop );
|
||||
sessionStorage.setItem( 'reveal-scroll-origin', location.origin + location.pathname );
|
||||
|
||||
this.storeScrollPositionTimeout = null;
|
||||
}, 50 );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores the scroll position when a deck is reloader.
|
||||
*/
|
||||
restoreScrollPosition() {
|
||||
|
||||
const scrollPosition = sessionStorage.getItem( 'reveal-scroll-top' );
|
||||
const scrollOrigin = sessionStorage.getItem( 'reveal-scroll-origin' );
|
||||
|
||||
if( scrollPosition && scrollOrigin === location.origin + location.pathname ) {
|
||||
this.viewportElement.scrollTop = parseInt( scrollPosition, 10 );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Activates the given page and starts its embedded content
|
||||
* if there is any.
|
||||
*
|
||||
* @param {object} page
|
||||
*/
|
||||
activatePage( page ) {
|
||||
|
||||
if( !page.active ) {
|
||||
|
||||
page.active = true;
|
||||
|
||||
const { slideElement, backgroundElement, contentElement, indexh, indexv } = page;
|
||||
|
||||
contentElement.style.display = 'block';
|
||||
|
||||
slideElement.classList.add( 'present' );
|
||||
|
||||
if( backgroundElement ) {
|
||||
backgroundElement.classList.add( 'present' );
|
||||
}
|
||||
|
||||
this.Reveal.setCurrentScrollPage( slideElement, indexh, indexv );
|
||||
this.Reveal.backgrounds.bubbleSlideContrastClassToElement( slideElement, this.viewportElement );
|
||||
|
||||
// If this page is part of an auto-animation there will be one
|
||||
// content element per auto-animated page. We need to show the
|
||||
// current page and hide all others.
|
||||
Array.from( contentElement.parentNode.querySelectorAll( '.scroll-page-content' ) ).forEach( sibling => {
|
||||
if( sibling !== contentElement ) {
|
||||
sibling.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Deactivates the page after it has been visible.
|
||||
*
|
||||
* @param {object} page
|
||||
*/
|
||||
deactivatePage( page ) {
|
||||
|
||||
if( page.active ) {
|
||||
|
||||
page.active = false;
|
||||
if( page.slideElement ) page.slideElement.classList.remove( 'present' );
|
||||
if( page.backgroundElement ) page.backgroundElement.classList.remove( 'present' );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
activateTrigger( trigger ) {
|
||||
|
||||
if( !trigger.active ) {
|
||||
trigger.active = true;
|
||||
trigger.activate();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
deactivateTrigger( trigger ) {
|
||||
|
||||
if( trigger.active ) {
|
||||
trigger.active = false;
|
||||
|
||||
if( trigger.deactivate ) {
|
||||
trigger.deactivate();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.getAllPages().find( page => {
|
||||
return page.indexh === h && page.indexv === v;
|
||||
} );
|
||||
|
||||
return page ? page.slideElement : null;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a list of all scroll triggers for the given slide
|
||||
* DOM element.
|
||||
*
|
||||
* @param {HTMLElement} slide
|
||||
* @returns {Array}
|
||||
*/
|
||||
getScrollTriggerBySlide( slide ) {
|
||||
|
||||
return this.slideTriggers.find( trigger => trigger.page.slideElement === slide );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of all pages in the scroll view. This includes
|
||||
* both top-level slides and auto-animate steps.
|
||||
*
|
||||
* @returns {Array}
|
||||
*/
|
||||
getAllPages() {
|
||||
|
||||
return this.pages.flatMap( page => [page, ...(page.autoAnimatePages || [])] );
|
||||
|
||||
}
|
||||
|
||||
onScroll() {
|
||||
|
||||
this.syncScrollPosition();
|
||||
this.storeScrollPosition();
|
||||
|
||||
}
|
||||
|
||||
get viewportElement() {
|
||||
|
||||
return this.Reveal.getViewportElement();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { extend, queryAll, closest, getMimeTypeFromFile } from '../utils/util.js'
|
||||
import { extend, queryAll, closest, getMimeTypeFromFile, encodeRFC3986URI } from '../utils/util.js'
|
||||
import { isMobile } from '../utils/device.js'
|
||||
|
||||
import fitty from 'fitty';
|
||||
@@ -25,6 +25,10 @@ export default class SlideContent {
|
||||
*/
|
||||
shouldPreload( element ) {
|
||||
|
||||
if( this.Reveal.isScrollView() ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Prefer an explicit global preload setting
|
||||
let preload = this.Reveal.getConfig().preloadIframes;
|
||||
|
||||
@@ -108,19 +112,21 @@ export default class SlideContent {
|
||||
// URL(s)
|
||||
else {
|
||||
backgroundContent.style.backgroundImage = backgroundImage.split( ',' ).map( background => {
|
||||
return `url(${encodeURI(background.trim())})`;
|
||||
// Decode URL(s) that are already encoded first
|
||||
let decoded = decodeURI(background.trim());
|
||||
return `url(${encodeRFC3986URI(decoded)})`;
|
||||
}).join( ',' );
|
||||
}
|
||||
}
|
||||
// Videos
|
||||
else if ( backgroundVideo && !this.Reveal.isSpeakerNotes() ) {
|
||||
else if ( backgroundVideo ) {
|
||||
let video = document.createElement( 'video' );
|
||||
|
||||
if( backgroundVideoLoop ) {
|
||||
video.setAttribute( 'loop', '' );
|
||||
}
|
||||
|
||||
if( backgroundVideoMuted ) {
|
||||
if( backgroundVideoMuted || this.Reveal.isSpeakerNotes() ) {
|
||||
video.muted = true;
|
||||
}
|
||||
|
||||
@@ -136,13 +142,15 @@ export default class SlideContent {
|
||||
|
||||
// Support comma separated lists of video sources
|
||||
backgroundVideo.split( ',' ).forEach( source => {
|
||||
const sourceElement = document.createElement( 'source' );
|
||||
sourceElement.setAttribute( 'src', source );
|
||||
|
||||
let type = getMimeTypeFromFile( source );
|
||||
if( type ) {
|
||||
video.innerHTML += `<source src="${source}" type="${type}">`;
|
||||
}
|
||||
else {
|
||||
video.innerHTML += `<source src="${source}">`;
|
||||
sourceElement.setAttribute( 'type', type );
|
||||
}
|
||||
|
||||
video.appendChild( sourceElement );
|
||||
} );
|
||||
|
||||
backgroundContent.appendChild( video );
|
||||
@@ -186,15 +194,14 @@ export default class SlideContent {
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies JS-dependent layout helpers for the given slide,
|
||||
* if there are any.
|
||||
* Applies JS-dependent layout helpers for the scope.
|
||||
*/
|
||||
layout( slide ) {
|
||||
layout( scopeElement ) {
|
||||
|
||||
// Autosize text with the r-fit-text class based on the
|
||||
// size of its container. This needs to happen after the
|
||||
// slide is visible in order to measure the text.
|
||||
Array.from( slide.querySelectorAll( '.r-fit-text' ) ).forEach( element => {
|
||||
Array.from( scopeElement.querySelectorAll( '.r-fit-text' ) ).forEach( element => {
|
||||
fitty( element, {
|
||||
minSize: 24,
|
||||
maxSize: this.Reveal.getConfig().height * 0.8,
|
||||
@@ -273,7 +280,9 @@ export default class SlideContent {
|
||||
*/
|
||||
startEmbeddedContent( element ) {
|
||||
|
||||
if( element && !this.Reveal.isSpeakerNotes() ) {
|
||||
if( element ) {
|
||||
|
||||
const isSpeakerNotesWindow = this.Reveal.isSpeakerNotes();
|
||||
|
||||
// Restart GIFs
|
||||
queryAll( element, 'img[src$=".gif"]' ).forEach( el => {
|
||||
@@ -299,6 +308,9 @@ export default class SlideContent {
|
||||
|
||||
if( autoplay && typeof el.play === 'function' ) {
|
||||
|
||||
// In the speaker view we only auto-play muted media
|
||||
if( isSpeakerNotesWindow && !el.muted ) return;
|
||||
|
||||
// If the media is ready, start playback
|
||||
if( el.readyState > 1 ) {
|
||||
this.startEmbeddedMedia( { target: el } );
|
||||
@@ -330,27 +342,33 @@ export default class SlideContent {
|
||||
}
|
||||
} );
|
||||
|
||||
// Normal iframes
|
||||
queryAll( element, 'iframe[src]' ).forEach( el => {
|
||||
if( closest( el, '.fragment' ) && !closest( el, '.fragment.visible' ) ) {
|
||||
return;
|
||||
}
|
||||
// Don't play iframe content in the speaker view since we can't
|
||||
// guarantee that it's muted
|
||||
if( !isSpeakerNotesWindow ) {
|
||||
|
||||
this.startEmbeddedIframe( { target: el } );
|
||||
} );
|
||||
// Normal iframes
|
||||
queryAll( element, 'iframe[src]' ).forEach( el => {
|
||||
if( closest( el, '.fragment' ) && !closest( el, '.fragment.visible' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Lazy loading iframes
|
||||
queryAll( element, 'iframe[data-src]' ).forEach( el => {
|
||||
if( closest( el, '.fragment' ) && !closest( el, '.fragment.visible' ) ) {
|
||||
return;
|
||||
}
|
||||
this.startEmbeddedIframe( { target: el } );
|
||||
} );
|
||||
|
||||
if( el.getAttribute( 'src' ) !== el.getAttribute( 'data-src' ) ) {
|
||||
el.removeEventListener( 'load', this.startEmbeddedIframe ); // remove first to avoid dupes
|
||||
el.addEventListener( 'load', this.startEmbeddedIframe );
|
||||
el.setAttribute( 'src', el.getAttribute( 'data-src' ) );
|
||||
}
|
||||
} );
|
||||
// Lazy loading iframes
|
||||
queryAll( element, 'iframe[data-src]' ).forEach( el => {
|
||||
if( closest( el, '.fragment' ) && !closest( el, '.fragment.visible' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if( el.getAttribute( 'src' ) !== el.getAttribute( 'data-src' ) ) {
|
||||
el.removeEventListener( 'load', this.startEmbeddedIframe ); // remove first to avoid dupes
|
||||
el.addEventListener( 'load', this.startEmbeddedIframe );
|
||||
el.setAttribute( 'src', el.getAttribute( 'data-src' ) );
|
||||
}
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -368,8 +386,11 @@ export default class SlideContent {
|
||||
isVisible = !!closest( event.target, '.present' );
|
||||
|
||||
if( isAttachedToDOM && isVisible ) {
|
||||
event.target.currentTime = 0;
|
||||
event.target.play();
|
||||
// Don't restart if media is already playing
|
||||
if( event.target.paused || event.target.ended ) {
|
||||
event.target.currentTime = 0;
|
||||
event.target.play();
|
||||
}
|
||||
}
|
||||
|
||||
event.target.removeEventListener( 'loadeddata', this.startEmbeddedMedia );
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
import {
|
||||
SLIDE_NUMBER_FORMAT_CURRENT,
|
||||
SLIDE_NUMBER_FORMAT_CURRENT_SLASH_TOTAL,
|
||||
SLIDE_NUMBER_FORMAT_HORIZONTAL_DOT_VERTICAL,
|
||||
SLIDE_NUMBER_FORMAT_HORIZONTAL_SLASH_VERTICAL
|
||||
} from "../utils/constants";
|
||||
|
||||
/**
|
||||
* Handles the display of reveal.js' optional slide number.
|
||||
*/
|
||||
@@ -23,7 +30,7 @@ export default class SlideNumber {
|
||||
configure( config, oldConfig ) {
|
||||
|
||||
let slideNumberDisplay = 'none';
|
||||
if( config.slideNumber && !this.Reveal.isPrintingPDF() ) {
|
||||
if( config.slideNumber && !this.Reveal.isPrintView() ) {
|
||||
if( config.showSlideNumber === 'all' ) {
|
||||
slideNumberDisplay = 'block';
|
||||
}
|
||||
@@ -56,7 +63,7 @@ export default class SlideNumber {
|
||||
|
||||
let config = this.Reveal.getConfig();
|
||||
let value;
|
||||
let format = 'h.v';
|
||||
let format = SLIDE_NUMBER_FORMAT_HORIZONTAL_DOT_VERTICAL;
|
||||
|
||||
if ( typeof config.slideNumber === 'function' ) {
|
||||
value = config.slideNumber( slide );
|
||||
@@ -69,7 +76,7 @@ export default class SlideNumber {
|
||||
// If there are ONLY vertical slides in this deck, always use
|
||||
// a flattened slide number
|
||||
if( !/c/.test( format ) && this.Reveal.getHorizontalSlides().length === 1 ) {
|
||||
format = 'c';
|
||||
format = SLIDE_NUMBER_FORMAT_CURRENT;
|
||||
}
|
||||
|
||||
// Offset the current slide number by 1 to make it 1-indexed
|
||||
@@ -77,16 +84,16 @@ export default class SlideNumber {
|
||||
|
||||
value = [];
|
||||
switch( format ) {
|
||||
case 'c':
|
||||
case SLIDE_NUMBER_FORMAT_CURRENT:
|
||||
value.push( this.Reveal.getSlidePastCount( slide ) + horizontalOffset );
|
||||
break;
|
||||
case 'c/t':
|
||||
case SLIDE_NUMBER_FORMAT_CURRENT_SLASH_TOTAL:
|
||||
value.push( this.Reveal.getSlidePastCount( slide ) + horizontalOffset, '/', this.Reveal.getTotalSlides() );
|
||||
break;
|
||||
default:
|
||||
let indices = this.Reveal.getIndices( slide );
|
||||
value.push( indices.h + horizontalOffset );
|
||||
let sep = format === 'h/v' ? '/' : '.';
|
||||
let sep = format === SLIDE_NUMBER_FORMAT_HORIZONTAL_SLASH_VERTICAL ? '/' : '.';
|
||||
if( this.Reveal.isVerticalSlide( slide ) ) value.push( sep, indices.v + 1 );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ export default class Touch {
|
||||
isSwipePrevented( target ) {
|
||||
|
||||
// Prevent accidental swipes when scrubbing timelines
|
||||
if( matches( target, 'video, audio' ) ) return true;
|
||||
if( matches( target, 'video[controls], audio[controls]' ) ) return true;
|
||||
|
||||
while( target && typeof target.hasAttribute === 'function' ) {
|
||||
if( target.hasAttribute( 'data-prevent-swipe' ) ) return true;
|
||||
@@ -103,6 +103,8 @@ export default class Touch {
|
||||
*/
|
||||
onTouchStart( event ) {
|
||||
|
||||
this.touchCaptured = false;
|
||||
|
||||
if( this.isSwipePrevented( event.target ) ) return true;
|
||||
|
||||
this.touchStartX = event.touches[0].clientX;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -44,7 +44,7 @@ export const colorToRgb = ( color ) => {
|
||||
};
|
||||
}
|
||||
|
||||
let rgba = color.match( /^rgba\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\,\s*([\d]+|[\d]*.[\d]+)\s*\)$/i );
|
||||
let rgba = color.match( /^rgba\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*([\d]+|[\d]*.[\d]+)\s*\)$/i );
|
||||
if( rgba ) {
|
||||
return {
|
||||
r: parseInt( rgba[1], 10 ),
|
||||
|
||||
@@ -2,9 +2,16 @@
|
||||
export const SLIDES_SELECTOR = '.slides section';
|
||||
export const HORIZONTAL_SLIDES_SELECTOR = '.slides>section';
|
||||
export const VERTICAL_SLIDES_SELECTOR = '.slides>section.present>section';
|
||||
export const HORIZONTAL_BACKGROUNDS_SELECTOR = '.backgrounds>.slide-background';
|
||||
|
||||
// Methods that may not be invoked via the postMessage API
|
||||
export const POST_MESSAGE_METHOD_BLACKLIST = /registerPlugin|registerKeyboardShortcut|addKeyBinding|addEventListener/;
|
||||
export const POST_MESSAGE_METHOD_BLACKLIST = /registerPlugin|registerKeyboardShortcut|addKeyBinding|addEventListener|showPreview/;
|
||||
|
||||
// Regex for retrieving the fragment style from a class attribute
|
||||
export const FRAGMENT_STYLE_REGEX = /fade-(down|up|right|left|out|in-then-out|in-then-semi-out)|semi-fade-out|current-visible|shrink|grow/;
|
||||
export const FRAGMENT_STYLE_REGEX = /fade-(down|up|right|left|out|in-then-out|in-then-semi-out)|semi-fade-out|current-visible|shrink|grow/;
|
||||
|
||||
// Slide number formats
|
||||
export const SLIDE_NUMBER_FORMAT_HORIZONTAL_DOT_VERTICAL = 'h.v';
|
||||
export const SLIDE_NUMBER_FORMAT_HORIZONTAL_SLASH_VERTICAL = 'h/v';
|
||||
export const SLIDE_NUMBER_FORMAT_CURRENT = 'c';
|
||||
export const SLIDE_NUMBER_FORMAT_CURRENT_SLASH_TOTAL = 'c/t';
|
||||
@@ -1,15 +1,8 @@
|
||||
const UA = navigator.userAgent;
|
||||
const testElement = document.createElement( 'div' );
|
||||
|
||||
export const isMobile = /(iphone|ipod|ipad|android)/gi.test( UA ) ||
|
||||
( navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1 ); // iPadOS
|
||||
|
||||
export const isChrome = /chrome/i.test( UA ) && !/edge/i.test( UA );
|
||||
|
||||
export const isAndroid = /android/gi.test( UA );
|
||||
|
||||
// Flags if we should use zoom instead of transform to scale
|
||||
// up slides. Zoom produces crisper results but has a lot of
|
||||
// xbrowser quirks so we only use it in whitelisted browsers.
|
||||
export const supportsZoom = 'zoom' in testElement.style && !isMobile &&
|
||||
( isChrome || /Version\/[\d\.]+.*Safari/.test( UA ) );
|
||||
export const isAndroid = /android/gi.test( UA );
|
||||
@@ -294,4 +294,20 @@ const fileExtensionToMimeMap = {
|
||||
*/
|
||||
export const getMimeTypeFromFile = ( filename='' ) => {
|
||||
return fileExtensionToMimeMap[filename.split('.').pop()]
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes a string for RFC3986-compliant URL format.
|
||||
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURI#encoding_for_rfc3986
|
||||
*
|
||||
* @param {string} url
|
||||
*/
|
||||
export const encodeRFC3986URI = ( url='' ) => {
|
||||
return encodeURI(url)
|
||||
.replace(/%5B/g, "[")
|
||||
.replace(/%5D/g, "]")
|
||||
.replace(
|
||||
/[!'()*]/g,
|
||||
(c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user